// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/5/LICENSE

/**
 * Supported keybindings:
 *   Too many to list. Refer to defaultKeymap below.
 *
 * Supported Ex commands:
 *   Refer to defaultExCommandMap below.
 *
 * Registers: unnamed, -, ., :, /, _, a-z, A-Z, 0-9
 *   (Does not respect the special case for number registers when delete
 *    operator is made with these commands: %, (, ),  , /, ?, n, N, {, } )
 *   TODO: Implement the remaining registers.
 *
 * Marks: a-z, A-Z, and 0-9
 *   TODO: Implement the remaining special marks. They have more complex
 *       behavior.
 *
 * Events:
 *  'vim-mode-change' - raised on the editor anytime the current mode changes,
 *                      Event object: {mode: "visual", subMode: "linewise"}
 *
 * Code structure:
 *  1. Default keymap
 *  2. Variable declarations and short basic helpers
 *  3. Instance (External API) implementation
 *  4. Internal state tracking objects (input state, counter) implementation
 *     and instantiation
 *  5. Key handler (the main command dispatcher) implementation
 *  6. Motion, operator, and action implementations
 *  7. Helper functions for the key handler, motions, operators, and actions
 *  8. Set up Vim to work as a keymap for CodeMirror.
 *  9. Ex command implementations.
 */

export function initVim(CodeMirror) {
    var Pos = CodeMirror.Pos

    function transformCursor(cm, range) {
        var vim = cm.state.vim
        if (!vim || vim.insertMode) return range.head
        var head = vim.sel.head
        if (!head) return range.head

        if (vim.visualBlock) {
            if (range.head.line != head.line) {
                return
            }
        }
        if (range.from() == range.anchor && !range.empty()) {
            if (range.head.line == head.line && range.head.ch != head.ch)
                return new Pos(range.head.line, range.head.ch - 1)
        }

        return range.head
    }

    function updateSelectionForSurrogateCharacters(cm, curStart, curEnd) {
        // start and character position when no selection
        // is the same in visual mode, and differs in 1 character in normal mode
        if (curStart.line === curEnd.line && curStart.ch >= curEnd.ch - 1) {
            var text = cm.getLine(curStart.line)
            var charCode = text.charCodeAt(curStart.ch)
            if (0xd800 <= charCode && charCode <= 0xd8ff) {
                curEnd.ch += 1
            }
        }

        return { start: curStart, end: curEnd }
    }

    var defaultKeymap = [
        // Key to key mapping. This goes first to make it possible to override
        // existing mappings.
        { keys: '<Left>', type: 'keyToKey', toKeys: 'h' },
        { keys: '<Right>', type: 'keyToKey', toKeys: 'l' },
        { keys: '<Up>', type: 'keyToKey', toKeys: 'k' },
        { keys: '<Down>', type: 'keyToKey', toKeys: 'j' },
        { keys: 'g<Up>', type: 'keyToKey', toKeys: 'gk' },
        { keys: 'g<Down>', type: 'keyToKey', toKeys: 'gj' },
        { keys: '<Space>', type: 'keyToKey', toKeys: 'l' },
        { keys: '<BS>', type: 'keyToKey', toKeys: 'h', context: 'normal' },
        { keys: '<Del>', type: 'keyToKey', toKeys: 'x', context: 'normal' },
        { keys: '<C-Space>', type: 'keyToKey', toKeys: 'W' },
        // { keys: '<C-BS>', type: 'keyToKey', toKeys: 'B', context: 'normal' },
        { keys: '<S-Space>', type: 'keyToKey', toKeys: 'w' },
        { keys: '<S-BS>', type: 'keyToKey', toKeys: 'b', context: 'normal' },
        { keys: '<C-n>', type: 'keyToKey', toKeys: 'j' },
        // { keys: '<C-p>', type: 'keyToKey', toKeys: 'k' },
        { keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>' },
        // { keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>' },
        { keys: '<C-[>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' },
        // { keys: '<C-c>', type: 'keyToKey', toKeys: '<Esc>', context: 'insert' },
        { keys: '<C-Esc>', type: 'keyToKey', toKeys: '<Esc>' }, // ipad keyboard sends C-Esc instead of C-[
        {
            keys: '<C-Esc>',
            type: 'keyToKey',
            toKeys: '<Esc>',
            context: 'insert',
        },
        { keys: 's', type: 'keyToKey', toKeys: 'cl', context: 'normal' },
        { keys: 's', type: 'keyToKey', toKeys: 'c', context: 'visual' },
        { keys: 'S', type: 'keyToKey', toKeys: 'cc', context: 'normal' },
        { keys: 'S', type: 'keyToKey', toKeys: 'VdO', context: 'visual' },
        { keys: '<Home>', type: 'keyToKey', toKeys: '0' },
        { keys: '<End>', type: 'keyToKey', toKeys: '$' },
        { keys: '<PageUp>', type: 'keyToKey', toKeys: '<C-b>' },
        { keys: '<PageDown>', type: 'keyToKey', toKeys: '<C-f>' },
        { keys: '<CR>', type: 'keyToKey', toKeys: 'j^', context: 'normal' },
        { keys: '<Ins>', type: 'keyToKey', toKeys: 'i', context: 'normal' },
        {
            keys: '<Ins>',
            type: 'action',
            action: 'toggleOverwrite',
            context: 'insert',
        },
        // Motions
        {
            keys: 'H',
            type: 'motion',
            motion: 'moveToTopLine',
            motionArgs: { linewise: true, toJumplist: true },
        },
        {
            keys: 'M',
            type: 'motion',
            motion: 'moveToMiddleLine',
            motionArgs: { linewise: true, toJumplist: true },
        },
        {
            keys: 'L',
            type: 'motion',
            motion: 'moveToBottomLine',
            motionArgs: { linewise: true, toJumplist: true },
        },
        {
            keys: 'h',
            type: 'motion',
            motion: 'moveByCharacters',
            motionArgs: { forward: false },
        },
        {
            keys: 'l',
            type: 'motion',
            motion: 'moveByCharacters',
            motionArgs: { forward: true },
        },
        {
            keys: 'j',
            type: 'motion',
            motion: 'moveByLines',
            motionArgs: { forward: true, linewise: true },
        },
        {
            keys: 'k',
            type: 'motion',
            motion: 'moveByLines',
            motionArgs: { forward: false, linewise: true },
        },
        {
            keys: 'gj',
            type: 'motion',
            motion: 'moveByDisplayLines',
            motionArgs: { forward: true },
        },
        {
            keys: 'gk',
            type: 'motion',
            motion: 'moveByDisplayLines',
            motionArgs: { forward: false },
        },
        {
            keys: 'w',
            type: 'motion',
            motion: 'moveByWords',
            motionArgs: { forward: true, wordEnd: false },
        },
        {
            keys: 'W',
            type: 'motion',
            motion: 'moveByWords',
            motionArgs: { forward: true, wordEnd: false, bigWord: true },
        },
        {
            keys: 'e',
            type: 'motion',
            motion: 'moveByWords',
            motionArgs: { forward: true, wordEnd: true, inclusive: true },
        },
        {
            keys: 'E',
            type: 'motion',
            motion: 'moveByWords',
            motionArgs: {
                forward: true,
                wordEnd: true,
                bigWord: true,
                inclusive: true,
            },
        },
        {
            keys: 'b',
            type: 'motion',
            motion: 'moveByWords',
            motionArgs: { forward: false, wordEnd: false },
        },
        {
            keys: 'B',
            type: 'motion',
            motion: 'moveByWords',
            motionArgs: { forward: false, wordEnd: false, bigWord: true },
        },
        {
            keys: 'ge',
            type: 'motion',
            motion: 'moveByWords',
            motionArgs: { forward: false, wordEnd: true, inclusive: true },
        },
        {
            keys: 'gE',
            type: 'motion',
            motion: 'moveByWords',
            motionArgs: {
                forward: false,
                wordEnd: true,
                bigWord: true,
                inclusive: true,
            },
        },
        {
            keys: '{',
            type: 'motion',
            motion: 'moveByParagraph',
            motionArgs: { forward: false, toJumplist: true },
        },
        {
            keys: '}',
            type: 'motion',
            motion: 'moveByParagraph',
            motionArgs: { forward: true, toJumplist: true },
        },
        {
            keys: '(',
            type: 'motion',
            motion: 'moveBySentence',
            motionArgs: { forward: false },
        },
        {
            keys: ')',
            type: 'motion',
            motion: 'moveBySentence',
            motionArgs: { forward: true },
        },
        {
            keys: '<C-f>',
            type: 'motion',
            motion: 'moveByPage',
            motionArgs: { forward: true },
        },
        {
            keys: '<C-b>',
            type: 'motion',
            motion: 'moveByPage',
            motionArgs: { forward: false },
        },
        {
            keys: '<C-d>',
            type: 'motion',
            motion: 'moveByScroll',
            motionArgs: { forward: true, explicitRepeat: true },
        },
        {
            keys: '<C-u>',
            type: 'motion',
            motion: 'moveByScroll',
            motionArgs: { forward: false, explicitRepeat: true },
        },
        {
            keys: 'gg',
            type: 'motion',
            motion: 'moveToLineOrEdgeOfDocument',
            motionArgs: {
                forward: false,
                explicitRepeat: true,
                linewise: true,
                toJumplist: true,
            },
        },
        {
            keys: 'G',
            type: 'motion',
            motion: 'moveToLineOrEdgeOfDocument',
            motionArgs: {
                forward: true,
                explicitRepeat: true,
                linewise: true,
                toJumplist: true,
            },
        },
        { keys: 'g$', type: 'motion', motion: 'moveToEndOfDisplayLine' },
        { keys: 'g^', type: 'motion', motion: 'moveToStartOfDisplayLine' },
        { keys: 'g0', type: 'motion', motion: 'moveToStartOfDisplayLine' },
        { keys: '0', type: 'motion', motion: 'moveToStartOfLine' },
        {
            keys: '^',
            type: 'motion',
            motion: 'moveToFirstNonWhiteSpaceCharacter',
        },
        {
            keys: '+',
            type: 'motion',
            motion: 'moveByLines',
            motionArgs: { forward: true, toFirstChar: true },
        },
        {
            keys: '-',
            type: 'motion',
            motion: 'moveByLines',
            motionArgs: { forward: false, toFirstChar: true },
        },
        {
            keys: '_',
            type: 'motion',
            motion: 'moveByLines',
            motionArgs: { forward: true, toFirstChar: true, repeatOffset: -1 },
        },
        {
            keys: '$',
            type: 'motion',
            motion: 'moveToEol',
            motionArgs: { inclusive: true },
        },
        {
            keys: '%',
            type: 'motion',
            motion: 'moveToMatchedSymbol',
            motionArgs: { inclusive: true, toJumplist: true },
        },
        {
            keys: 'f<character>',
            type: 'motion',
            motion: 'moveToCharacter',
            motionArgs: { forward: true, inclusive: true },
        },
        {
            keys: 'F<character>',
            type: 'motion',
            motion: 'moveToCharacter',
            motionArgs: { forward: false },
        },
        {
            keys: 't<character>',
            type: 'motion',
            motion: 'moveTillCharacter',
            motionArgs: { forward: true, inclusive: true },
        },
        {
            keys: 'T<character>',
            type: 'motion',
            motion: 'moveTillCharacter',
            motionArgs: { forward: false },
        },
        {
            keys: ';',
            type: 'motion',
            motion: 'repeatLastCharacterSearch',
            motionArgs: { forward: true },
        },
        {
            keys: ',',
            type: 'motion',
            motion: 'repeatLastCharacterSearch',
            motionArgs: { forward: false },
        },
        {
            keys: "'<character>",
            type: 'motion',
            motion: 'goToMark',
            motionArgs: { toJumplist: true, linewise: true },
        },
        {
            keys: '`<character>',
            type: 'motion',
            motion: 'goToMark',
            motionArgs: { toJumplist: true },
        },
        {
            keys: ']`',
            type: 'motion',
            motion: 'jumpToMark',
            motionArgs: { forward: true },
        },
        {
            keys: '[`',
            type: 'motion',
            motion: 'jumpToMark',
            motionArgs: { forward: false },
        },
        {
            keys: "]'",
            type: 'motion',
            motion: 'jumpToMark',
            motionArgs: { forward: true, linewise: true },
        },
        {
            keys: "['",
            type: 'motion',
            motion: 'jumpToMark',
            motionArgs: { forward: false, linewise: true },
        },
        // the next two aren't motions but must come before more general motion declarations
        {
            keys: ']p',
            type: 'action',
            action: 'paste',
            isEdit: true,
            actionArgs: { after: true, isEdit: true, matchIndent: true },
        },
        {
            keys: '[p',
            type: 'action',
            action: 'paste',
            isEdit: true,
            actionArgs: { after: false, isEdit: true, matchIndent: true },
        },
        {
            keys: ']<character>',
            type: 'motion',
            motion: 'moveToSymbol',
            motionArgs: { forward: true, toJumplist: true },
        },
        {
            keys: '[<character>',
            type: 'motion',
            motion: 'moveToSymbol',
            motionArgs: { forward: false, toJumplist: true },
        },
        { keys: '|', type: 'motion', motion: 'moveToColumn' },
        {
            keys: 'o',
            type: 'motion',
            motion: 'moveToOtherHighlightedEnd',
            context: 'visual',
        },
        {
            keys: 'O',
            type: 'motion',
            motion: 'moveToOtherHighlightedEnd',
            motionArgs: { sameLine: true },
            context: 'visual',
        },
        // Operators
        { keys: 'd', type: 'operator', operator: 'delete' },
        { keys: 'y', type: 'operator', operator: 'yank' },
        { keys: 'c', type: 'operator', operator: 'change' },
        { keys: '=', type: 'operator', operator: 'indentAuto' },
        {
            keys: '>',
            type: 'operator',
            operator: 'indent',
            operatorArgs: { indentRight: true },
        },
        {
            keys: '<',
            type: 'operator',
            operator: 'indent',
            operatorArgs: { indentRight: false },
        },
        { keys: 'g~', type: 'operator', operator: 'changeCase' },
        {
            keys: 'gu',
            type: 'operator',
            operator: 'changeCase',
            operatorArgs: { toLower: true },
            isEdit: true,
        },
        {
            keys: 'gU',
            type: 'operator',
            operator: 'changeCase',
            operatorArgs: { toLower: false },
            isEdit: true,
        },
        {
            keys: 'n',
            type: 'motion',
            motion: 'findNext',
            motionArgs: { forward: true, toJumplist: true },
        },
        {
            keys: 'N',
            type: 'motion',
            motion: 'findNext',
            motionArgs: { forward: false, toJumplist: true },
        },
        {
            keys: 'gn',
            type: 'motion',
            motion: 'findAndSelectNextInclusive',
            motionArgs: { forward: true },
        },
        {
            keys: 'gN',
            type: 'motion',
            motion: 'findAndSelectNextInclusive',
            motionArgs: { forward: false },
        },
        // Operator-Motion dual commands
        {
            keys: 'x',
            type: 'operatorMotion',
            operator: 'delete',
            motion: 'moveByCharacters',
            motionArgs: { forward: true },
            operatorMotionArgs: { visualLine: false },
        },
        {
            keys: 'X',
            type: 'operatorMotion',
            operator: 'delete',
            motion: 'moveByCharacters',
            motionArgs: { forward: false },
            operatorMotionArgs: { visualLine: true },
        },
        {
            keys: 'D',
            type: 'operatorMotion',
            operator: 'delete',
            motion: 'moveToEol',
            motionArgs: { inclusive: true },
            context: 'normal',
        },
        {
            keys: 'D',
            type: 'operator',
            operator: 'delete',
            operatorArgs: { linewise: true },
            context: 'visual',
        },
        {
            keys: 'Y',
            type: 'operatorMotion',
            operator: 'yank',
            motion: 'expandToLine',
            motionArgs: { linewise: true },
            context: 'normal',
        },
        {
            keys: 'Y',
            type: 'operator',
            operator: 'yank',
            operatorArgs: { linewise: true },
            context: 'visual',
        },
        {
            keys: 'C',
            type: 'operatorMotion',
            operator: 'change',
            motion: 'moveToEol',
            motionArgs: { inclusive: true },
            context: 'normal',
        },
        {
            keys: 'C',
            type: 'operator',
            operator: 'change',
            operatorArgs: { linewise: true },
            context: 'visual',
        },
        {
            keys: '~',
            type: 'operatorMotion',
            operator: 'changeCase',
            motion: 'moveByCharacters',
            motionArgs: { forward: true },
            operatorArgs: { shouldMoveCursor: true },
            context: 'normal',
        },
        {
            keys: '~',
            type: 'operator',
            operator: 'changeCase',
            context: 'visual',
        },
        {
            keys: '<C-u>',
            type: 'operatorMotion',
            operator: 'delete',
            motion: 'moveToStartOfLine',
            context: 'insert',
        },
        {
            keys: '<C-w>',
            type: 'operatorMotion',
            operator: 'delete',
            motion: 'moveByWords',
            motionArgs: { forward: false, wordEnd: false },
            context: 'insert',
        },
        //ignore C-w in normal mode
        // { keys: '<C-w>', type: 'idle', context: 'normal' },
        // Actions
        {
            keys: '<C-i>',
            type: 'action',
            action: 'jumpListWalk',
            actionArgs: { forward: true },
        },
        // {
        //     keys: '<C-o>',
        //     type: 'action',
        //     action: 'jumpListWalk',
        //     actionArgs: { forward: false },
        // },
        {
            keys: '<C-e>',
            type: 'action',
            action: 'scroll',
            actionArgs: { forward: true, linewise: true },
        },
        {
            keys: '<C-y>',
            type: 'action',
            action: 'scroll',
            actionArgs: { forward: false, linewise: true },
        },
        {
            keys: 'a',
            type: 'action',
            action: 'enterInsertMode',
            isEdit: true,
            actionArgs: { insertAt: 'charAfter' },
            context: 'normal',
        },
        {
            keys: 'A',
            type: 'action',
            action: 'enterInsertMode',
            isEdit: true,
            actionArgs: { insertAt: 'eol' },
            context: 'normal',
        },
        {
            keys: 'A',
            type: 'action',
            action: 'enterInsertMode',
            isEdit: true,
            actionArgs: { insertAt: 'endOfSelectedArea' },
            context: 'visual',
        },
        {
            keys: 'i',
            type: 'action',
            action: 'enterInsertMode',
            isEdit: true,
            actionArgs: { insertAt: 'inplace' },
            context: 'normal',
        },
        {
            keys: 'gi',
            type: 'action',
            action: 'enterInsertMode',
            isEdit: true,
            actionArgs: { insertAt: 'lastEdit' },
            context: 'normal',
        },
        {
            keys: 'I',
            type: 'action',
            action: 'enterInsertMode',
            isEdit: true,
            actionArgs: { insertAt: 'firstNonBlank' },
            context: 'normal',
        },
        {
            keys: 'gI',
            type: 'action',
            action: 'enterInsertMode',
            isEdit: true,
            actionArgs: { insertAt: 'bol' },
            context: 'normal',
        },
        {
            keys: 'I',
            type: 'action',
            action: 'enterInsertMode',
            isEdit: true,
            actionArgs: { insertAt: 'startOfSelectedArea' },
            context: 'visual',
        },
        {
            keys: 'o',
            type: 'action',
            action: 'newLineAndEnterInsertMode',
            isEdit: true,
            interlaceInsertRepeat: true,
            actionArgs: { after: true },
            context: 'normal',
        },
        {
            keys: 'O',
            type: 'action',
            action: 'newLineAndEnterInsertMode',
            isEdit: true,
            interlaceInsertRepeat: true,
            actionArgs: { after: false },
            context: 'normal',
        },
        { keys: 'v', type: 'action', action: 'toggleVisualMode' },
        {
            keys: 'V',
            type: 'action',
            action: 'toggleVisualMode',
            actionArgs: { linewise: true },
        },
        // {
        //     keys: '<C-v>',
        //     type: 'action',
        //     action: 'toggleVisualMode',
        //     actionArgs: { blockwise: true },
        // },
        {
            keys: '<C-q>',
            type: 'action',
            action: 'toggleVisualMode',
            actionArgs: { blockwise: true },
        },
        { keys: 'gv', type: 'action', action: 'reselectLastSelection' },
        { keys: 'J', type: 'action', action: 'joinLines', isEdit: true },
        {
            keys: 'gJ',
            type: 'action',
            action: 'joinLines',
            actionArgs: { keepSpaces: true },
            isEdit: true,
        },
        {
            keys: 'p',
            type: 'action',
            action: 'paste',
            isEdit: true,
            actionArgs: { after: true, isEdit: true },
        },
        {
            keys: 'P',
            type: 'action',
            action: 'paste',
            isEdit: true,
            actionArgs: { after: false, isEdit: true },
        },
        {
            keys: 'r<character>',
            type: 'action',
            action: 'replace',
            isEdit: true,
        },
        { keys: '@<character>', type: 'action', action: 'replayMacro' },
        {
            keys: 'q<character>',
            type: 'action',
            action: 'enterMacroRecordMode',
        },
        // Handle Replace-mode as a special case of insert mode.
        {
            keys: 'R',
            type: 'action',
            action: 'enterInsertMode',
            isEdit: true,
            actionArgs: { replace: true },
            context: 'normal',
        },
        {
            keys: 'R',
            type: 'operator',
            operator: 'change',
            operatorArgs: { linewise: true, fullLine: true },
            context: 'visual',
            exitVisualBlock: true,
        },
        { keys: 'u', type: 'action', action: 'undo', context: 'normal' },
        {
            keys: 'u',
            type: 'operator',
            operator: 'changeCase',
            operatorArgs: { toLower: true },
            context: 'visual',
            isEdit: true,
        },
        {
            keys: 'U',
            type: 'operator',
            operator: 'changeCase',
            operatorArgs: { toLower: false },
            context: 'visual',
            isEdit: true,
        },
        { keys: '<C-r>', type: 'action', action: 'redo' },
        { keys: 'm<character>', type: 'action', action: 'setMark' },
        { keys: '"<character>', type: 'action', action: 'setRegister' },
        {
            keys: '<C-r><character>',
            type: 'action',
            action: 'insertRegister',
            context: 'insert',
            isEdit: true,
        },
        {
            keys: 'zz',
            type: 'action',
            action: 'scrollToCursor',
            actionArgs: { position: 'center' },
        },
        {
            keys: 'z.',
            type: 'action',
            action: 'scrollToCursor',
            actionArgs: { position: 'center' },
            motion: 'moveToFirstNonWhiteSpaceCharacter',
        },
        {
            keys: 'zt',
            type: 'action',
            action: 'scrollToCursor',
            actionArgs: { position: 'top' },
        },
        {
            keys: 'z<CR>',
            type: 'action',
            action: 'scrollToCursor',
            actionArgs: { position: 'top' },
            motion: 'moveToFirstNonWhiteSpaceCharacter',
        },
        {
            keys: 'zb',
            type: 'action',
            action: 'scrollToCursor',
            actionArgs: { position: 'bottom' },
        },
        {
            keys: 'z-',
            type: 'action',
            action: 'scrollToCursor',
            actionArgs: { position: 'bottom' },
            motion: 'moveToFirstNonWhiteSpaceCharacter',
        },
        { keys: '.', type: 'action', action: 'repeatLastEdit' },
        // {
        //     keys: '<C-a>',
        //     type: 'action',
        //     action: 'incrementNumberToken',
        //     isEdit: true,
        //     actionArgs: { increase: true, backtrack: false },
        // },
        {
            keys: '<C-x>',
            type: 'action',
            action: 'incrementNumberToken',
            isEdit: true,
            actionArgs: { increase: false, backtrack: false },
        },
        {
            keys: '<C-t>',
            type: 'action',
            action: 'indent',
            actionArgs: { indentRight: true },
            context: 'insert',
        },
        {
            keys: '<C-d>',
            type: 'action',
            action: 'indent',
            actionArgs: { indentRight: false },
            context: 'insert',
        },
        // Text object motions
        {
            keys: 'a<character>',
            type: 'motion',
            motion: 'textObjectManipulation',
        },
        {
            keys: 'i<character>',
            type: 'motion',
            motion: 'textObjectManipulation',
            motionArgs: { textObjectInner: true },
        },
        // Search
        {
            keys: '/',
            type: 'search',
            searchArgs: { forward: true, querySrc: 'prompt', toJumplist: true },
        },
        {
            keys: '?',
            type: 'search',
            searchArgs: {
                forward: false,
                querySrc: 'prompt',
                toJumplist: true,
            },
        },
        {
            keys: '*',
            type: 'search',
            searchArgs: {
                forward: true,
                querySrc: 'wordUnderCursor',
                wholeWordOnly: true,
                toJumplist: true,
            },
        },
        {
            keys: '#',
            type: 'search',
            searchArgs: {
                forward: false,
                querySrc: 'wordUnderCursor',
                wholeWordOnly: true,
                toJumplist: true,
            },
        },
        {
            keys: 'g*',
            type: 'search',
            searchArgs: {
                forward: true,
                querySrc: 'wordUnderCursor',
                toJumplist: true,
            },
        },
        {
            keys: 'g#',
            type: 'search',
            searchArgs: {
                forward: false,
                querySrc: 'wordUnderCursor',
                toJumplist: true,
            },
        },
        // Ex command
        { keys: ':', type: 'ex' },
    ]
    var defaultKeymapLength = defaultKeymap.length

    /**
     * Ex commands
     * Care must be taken when adding to the default Ex command map. For any
     * pair of commands that have a shared prefix, at least one of their
     * shortNames must not match the prefix of the other command.
     */
    var defaultExCommandMap = [
        { name: 'colorscheme', shortName: 'colo' },
        { name: 'map' },
        { name: 'imap', shortName: 'im' },
        { name: 'nmap', shortName: 'nm' },
        { name: 'vmap', shortName: 'vm' },
        { name: 'unmap' },
        { name: 'write', shortName: 'w' },
        { name: 'undo', shortName: 'u' },
        { name: 'redo', shortName: 'red' },
        { name: 'set', shortName: 'se' },
        { name: 'setlocal', shortName: 'setl' },
        { name: 'setglobal', shortName: 'setg' },
        { name: 'sort', shortName: 'sor' },
        { name: 'substitute', shortName: 's', possiblyAsync: true },
        { name: 'nohlsearch', shortName: 'noh' },
        { name: 'yank', shortName: 'y' },
        { name: 'delmarks', shortName: 'delm' },
        {
            name: 'registers',
            shortName: 'reg',
            excludeFromCommandHistory: true,
        },
        { name: 'vglobal', shortName: 'v' },
        { name: 'global', shortName: 'g' },
    ]

    function enterVimMode(cm) {
        cm.setOption('disableInput', true)
        cm.setOption('showCursorWhenSelecting', false)
        CodeMirror.signal(cm, 'vim-mode-change', { mode: 'normal' })
        cm.on('cursorActivity', onCursorActivity)
        maybeInitVimState(cm)
        CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm))
    }

    function leaveVimMode(cm) {
        cm.setOption('disableInput', false)
        cm.off('cursorActivity', onCursorActivity)
        CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm))
        cm.state.vim = null
        if (highlightTimeout) clearTimeout(highlightTimeout)
    }

    function detachVimMap(cm, next) {
        if (this == CodeMirror.keyMap.vim) {
            cm.options.$customCursor = null
            CodeMirror.rmClass(cm.getWrapperElement(), 'cm-fat-cursor')
        }

        if (!next || next.attach != attachVimMap) leaveVimMode(cm)
    }
    function attachVimMap(cm, prev) {
        if (this == CodeMirror.keyMap.vim) {
            if (cm.curOp) cm.curOp.selectionChanged = true
            cm.options.$customCursor = transformCursor
            CodeMirror.addClass(cm.getWrapperElement(), 'cm-fat-cursor')
        }

        if (!prev || prev.attach != attachVimMap) enterVimMode(cm)
    }

    // Deprecated, simply setting the keymap works again.
    CodeMirror.defineOption('vimMode', false, function (cm, val, prev) {
        if (val && cm.getOption('keyMap') != 'vim')
            cm.setOption('keyMap', 'vim')
        else if (
            !val &&
            prev != CodeMirror.Init &&
            /^vim/.test(cm.getOption('keyMap'))
        )
            cm.setOption('keyMap', 'default')
    })

    function cmKey(key, cm) {
        if (!cm) {
            return undefined
        }
        if (this[key]) {
            return this[key]
        }
        var vimKey = cmKeyToVimKey(key)
        if (!vimKey) {
            return false
        }
        var cmd = vimApi.findKey(cm, vimKey)
        if (typeof cmd == 'function') {
            CodeMirror.signal(cm, 'vim-keypress', vimKey)
        }
        return cmd
    }

    var modifiers = {
        Shift: 'S',
        Ctrl: 'C',
        Alt: 'A',
        Cmd: 'D',
        Mod: 'A',
        CapsLock: '',
    }
    var specialKeys = {
        Enter: 'CR',
        Backspace: 'BS',
        Delete: 'Del',
        Insert: 'Ins',
    }
    function cmKeyToVimKey(key) {
        if (key.charAt(0) == "'") {
            // Keypress character binding of format "'a'"
            return key.charAt(1)
        }
        var pieces = key.split(/-(?!$)/)
        var lastPiece = pieces[pieces.length - 1]
        if (pieces.length == 1 && pieces[0].length == 1) {
            // No-modifier bindings use literal character bindings above. Skip.
            return false
        } else if (
            pieces.length == 2 &&
            pieces[0] == 'Shift' &&
            lastPiece.length == 1
        ) {
            // Ignore Shift+char bindings as they should be handled by literal character.
            return false
        }
        var hasCharacter = false
        for (var i = 0; i < pieces.length; i++) {
            var piece = pieces[i]
            if (piece in modifiers) {
                pieces[i] = modifiers[piece]
            } else {
                hasCharacter = true
            }
            if (piece in specialKeys) {
                pieces[i] = specialKeys[piece]
            }
        }
        if (!hasCharacter) {
            // Vim does not support modifier only keys.
            return false
        }
        // TODO: Current bindings expect the character to be lower case, but
        // it looks like vim key notation uses upper case.
        if (isUpperCase(lastPiece)) {
            pieces[pieces.length - 1] = lastPiece.toLowerCase()
        }
        return '<' + pieces.join('-') + '>'
    }

    function getOnPasteFn(cm) {
        var vim = cm.state.vim
        if (!vim.onPasteFn) {
            vim.onPasteFn = function () {
                if (!vim.insertMode) {
                    cm.setCursor(offsetCursor(cm.getCursor(), 0, 1))
                    actions.enterInsertMode(cm, {}, vim)
                }
            }
        }
        return vim.onPasteFn
    }

    var numberRegex = /[\d]/
    var wordCharTest = [
            CodeMirror.isWordChar,
            function (ch) {
                return ch && !CodeMirror.isWordChar(ch) && !/\s/.test(ch)
            },
        ],
        bigWordCharTest = [
            function (ch) {
                return /\S/.test(ch)
            },
        ]
    function makeKeyRange(start, size) {
        var keys = []
        for (var i = start; i < start + size; i++) {
            keys.push(String.fromCharCode(i))
        }
        return keys
    }
    var upperCaseAlphabet = makeKeyRange(65, 26)
    var lowerCaseAlphabet = makeKeyRange(97, 26)
    var numbers = makeKeyRange(48, 10)
    var validMarks = [].concat(upperCaseAlphabet, lowerCaseAlphabet, numbers, [
        '<',
        '>',
    ])
    var validRegisters = [].concat(
        upperCaseAlphabet,
        lowerCaseAlphabet,
        numbers,
        ['-', '"', '.', ':', '_', '/', '+']
    )
    var upperCaseChars
    try {
        upperCaseChars = new RegExp('^[\\p{Lu}]$', 'u')
    } catch (_) {
        upperCaseChars = /^[A-Z]$/
    }

    function isLine(cm, line) {
        return line >= cm.firstLine() && line <= cm.lastLine()
    }
    function isLowerCase(k) {
        return /^[a-z]$/.test(k)
    }
    function isMatchableSymbol(k) {
        return '()[]{}'.indexOf(k) != -1
    }
    function isNumber(k) {
        return numberRegex.test(k)
    }
    function isUpperCase(k) {
        return upperCaseChars.test(k)
    }
    function isWhiteSpaceString(k) {
        return /^\s*$/.test(k)
    }
    function isEndOfSentenceSymbol(k) {
        return '.?!'.indexOf(k) != -1
    }
    function inArray(val, arr) {
        for (var i = 0; i < arr.length; i++) {
            if (arr[i] == val) {
                return true
            }
        }
        return false
    }

    var options = {}
    function defineOption(name, defaultValue, type, aliases, callback) {
        if (defaultValue === undefined && !callback) {
            throw Error('defaultValue is required unless callback is provided')
        }
        if (!type) {
            type = 'string'
        }
        options[name] = {
            type: type,
            defaultValue: defaultValue,
            callback: callback,
        }
        if (aliases) {
            for (var i = 0; i < aliases.length; i++) {
                options[aliases[i]] = options[name]
            }
        }
        if (defaultValue) {
            setOption(name, defaultValue)
        }
    }

    function setOption(name, value, cm, cfg) {
        var option = options[name]
        cfg = cfg || {}
        var scope = cfg.scope
        if (!option) {
            return new Error('Unknown option: ' + name)
        }
        if (option.type == 'boolean') {
            if (value && value !== true) {
                return new Error('Invalid argument: ' + name + '=' + value)
            } else if (value !== false) {
                // Boolean options are set to true if value is not defined.
                value = true
            }
        }
        if (option.callback) {
            if (scope !== 'local') {
                option.callback(value, undefined)
            }
            if (scope !== 'global' && cm) {
                option.callback(value, cm)
            }
        } else {
            if (scope !== 'local') {
                option.value = option.type == 'boolean' ? !!value : value
            }
            if (scope !== 'global' && cm) {
                cm.state.vim.options[name] = { value: value }
            }
        }
    }

    function getOption(name, cm, cfg) {
        var option = options[name]
        cfg = cfg || {}
        var scope = cfg.scope
        if (!option) {
            return new Error('Unknown option: ' + name)
        }
        if (option.callback) {
            var local = cm && option.callback(undefined, cm)
            if (scope !== 'global' && local !== undefined) {
                return local
            }
            if (scope !== 'local') {
                return option.callback()
            }
            return
        } else {
            var local = scope !== 'global' && cm && cm.state.vim.options[name]
            return (local || (scope !== 'local' && option) || {}).value
        }
    }

    defineOption('filetype', undefined, 'string', ['ft'], function (name, cm) {
        // Option is local. Do nothing for global.
        if (cm === undefined) {
            return
        }
        // The 'filetype' option proxies to the CodeMirror 'mode' option.
        if (name === undefined) {
            var mode = cm.getOption('mode')
            return mode == 'null' ? '' : mode
        } else {
            var mode = name == '' ? 'null' : name
            cm.setOption('mode', mode)
        }
    })

    var createCircularJumpList = function () {
        var size = 100
        var pointer = -1
        var head = 0
        var tail = 0
        var buffer = new Array(size)
        function add(cm, oldCur, newCur) {
            var current = pointer % size
            var curMark = buffer[current]
            function useNextSlot(cursor) {
                var next = ++pointer % size
                var trashMark = buffer[next]
                if (trashMark) {
                    trashMark.clear()
                }
                buffer[next] = cm.setBookmark(cursor)
            }
            if (curMark) {
                var markPos = curMark.find()
                // avoid recording redundant cursor position
                if (markPos && !cursorEqual(markPos, oldCur)) {
                    useNextSlot(oldCur)
                }
            } else {
                useNextSlot(oldCur)
            }
            useNextSlot(newCur)
            head = pointer
            tail = pointer - size + 1
            if (tail < 0) {
                tail = 0
            }
        }
        function move(cm, offset) {
            pointer += offset
            if (pointer > head) {
                pointer = head
            } else if (pointer < tail) {
                pointer = tail
            }
            var mark = buffer[(size + pointer) % size]
            // skip marks that are temporarily removed from text buffer
            if (mark && !mark.find()) {
                var inc = offset > 0 ? 1 : -1
                var newCur
                var oldCur = cm.getCursor()
                do {
                    pointer += inc
                    mark = buffer[(size + pointer) % size]
                    // skip marks that are the same as current position
                    if (
                        mark &&
                        (newCur = mark.find()) &&
                        !cursorEqual(oldCur, newCur)
                    ) {
                        break
                    }
                } while (pointer < head && pointer > tail)
            }
            return mark
        }
        function find(cm, offset) {
            var oldPointer = pointer
            var mark = move(cm, offset)
            pointer = oldPointer
            return mark && mark.find()
        }
        return {
            cachedCursor: undefined, //used for # and * jumps
            add: add,
            find: find,
            move: move,
        }
    }

    // Returns an object to track the changes associated insert mode.  It
    // clones the object that is passed in, or creates an empty object one if
    // none is provided.
    var createInsertModeChanges = function (c) {
        if (c) {
            // Copy construction
            return {
                changes: c.changes,
                expectCursorActivityForChange: c.expectCursorActivityForChange,
            }
        }
        return {
            // Change list
            changes: [],
            // Set to true on change, false on cursorActivity.
            expectCursorActivityForChange: false,
        }
    }

    function MacroModeState() {
        this.latestRegister = undefined
        this.isPlaying = false
        this.isRecording = false
        this.replaySearchQueries = []
        this.onRecordingDone = undefined
        this.lastInsertModeChanges = createInsertModeChanges()
    }
    MacroModeState.prototype = {
        exitMacroRecordMode: function () {
            var macroModeState = vimGlobalState.macroModeState
            if (macroModeState.onRecordingDone) {
                macroModeState.onRecordingDone() // close dialog
            }
            macroModeState.onRecordingDone = undefined
            macroModeState.isRecording = false
        },
        enterMacroRecordMode: function (cm, registerName) {
            var register =
                vimGlobalState.registerController.getRegister(registerName)
            if (register) {
                register.clear()
                this.latestRegister = registerName
                if (cm.openDialog) {
                    var template = dom(
                        'span',
                        { class: 'cm-vim-message' },
                        'recording @' + registerName
                    )
                    this.onRecordingDone = cm.openDialog(template, null, {
                        bottom: true,
                    })
                }
                this.isRecording = true
            }
        },
    }

    function maybeInitVimState(cm) {
        if (!cm.state.vim) {
            // Store instance state in the CodeMirror object.
            cm.state.vim = {
                inputState: new InputState(),
                // Vim's input state that triggered the last edit, used to repeat
                // motions and operators with '.'.
                lastEditInputState: undefined,
                // Vim's action command before the last edit, used to repeat actions
                // with '.' and insert mode repeat.
                lastEditActionCommand: undefined,
                // When using jk for navigation, if you move from a longer line to a
                // shorter line, the cursor may clip to the end of the shorter line.
                // If j is pressed again and cursor goes to the next line, the
                // cursor should go back to its horizontal position on the longer
                // line if it can. This is to keep track of the horizontal position.
                lastHPos: -1,
                // Doing the same with screen-position for gj/gk
                lastHSPos: -1,
                // The last motion command run. Cleared if a non-motion command gets
                // executed in between.
                lastMotion: null,
                marks: {},
                insertMode: false,
                // Repeat count for changes made in insert mode, triggered by key
                // sequences like 3,i. Only exists when insertMode is true.
                insertModeRepeat: undefined,
                visualMode: false,
                // If we are in visual line mode. No effect if visualMode is false.
                visualLine: false,
                visualBlock: false,
                lastSelection: null,
                lastPastedText: null,
                sel: {},
                // Buffer-local/window-local values of vim options.
                options: {},
            }
        }
        return cm.state.vim
    }
    var vimGlobalState
    function resetVimGlobalState() {
        vimGlobalState = {
            // The current search query.
            searchQuery: null,
            // Whether we are searching backwards.
            searchIsReversed: false,
            // Replace part of the last substituted pattern
            lastSubstituteReplacePart: undefined,
            jumpList: createCircularJumpList(),
            macroModeState: new MacroModeState(),
            // Recording latest f, t, F or T motion command.
            lastCharacterSearch: {
                increment: 0,
                forward: true,
                selectedCharacter: '',
            },
            registerController: new RegisterController({}),
            // search history buffer
            searchHistoryController: new HistoryController(),
            // ex Command history buffer
            exCommandHistoryController: new HistoryController(),
        }
        for (var optionName in options) {
            var option = options[optionName]
            option.value = option.defaultValue
        }
    }

    var lastInsertModeKeyTimer
    var vimApi = {
        enterVimMode: enterVimMode,
        leaveVimMode: leaveVimMode,
        buildKeyMap: function () {
            // TODO: Convert keymap into dictionary format for fast lookup.
        },
        // Testing hook, though it might be useful to expose the register
        // controller anyway.
        getRegisterController: function () {
            return vimGlobalState.registerController
        },
        // Testing hook.
        resetVimGlobalState_: resetVimGlobalState,

        // Testing hook.
        getVimGlobalState_: function () {
            return vimGlobalState
        },

        // Testing hook.
        maybeInitVimState_: maybeInitVimState,

        suppressErrorLogging: false,

        InsertModeKey: InsertModeKey,
        map: function (lhs, rhs, ctx) {
            // Add user defined key bindings.
            exCommandDispatcher.map(lhs, rhs, ctx)
        },
        unmap: function (lhs, ctx) {
            return exCommandDispatcher.unmap(lhs, ctx)
        },
        // Non-recursive map function.
        // NOTE: This will not create mappings to key maps that aren't present
        // in the default key map. See TODO at bottom of function.
        noremap: function (lhs, rhs, ctx) {
            function toCtxArray(ctx) {
                return ctx ? [ctx] : ['normal', 'insert', 'visual']
            }
            var ctxsToMap = toCtxArray(ctx)
            // Look through all actual defaults to find a map candidate.
            var actualLength = defaultKeymap.length,
                origLength = defaultKeymapLength
            for (
                var i = actualLength - origLength;
                i < actualLength && ctxsToMap.length;
                i++
            ) {
                var mapping = defaultKeymap[i]
                // Omit mappings that operate in the wrong context(s) and those of invalid type.
                if (
                    mapping.keys == rhs &&
                    (!ctx || !mapping.context || mapping.context === ctx) &&
                    mapping.type.substr(0, 2) !== 'ex' &&
                    mapping.type.substr(0, 3) !== 'key'
                ) {
                    // Make a shallow copy of the original keymap entry.
                    var newMapping = {}
                    for (var key in mapping) {
                        newMapping[key] = mapping[key]
                    }
                    // Modify it point to the new mapping with the proper context.
                    newMapping.keys = lhs
                    if (ctx && !newMapping.context) {
                        newMapping.context = ctx
                    }
                    // Add it to the keymap with a higher priority than the original.
                    this._mapCommand(newMapping)
                    // Record the mapped contexts as complete.
                    var mappedCtxs = toCtxArray(mapping.context)
                    ctxsToMap = ctxsToMap.filter(function (el) {
                        return mappedCtxs.indexOf(el) === -1
                    })
                }
            }
            // TODO: Create non-recursive keyToKey mappings for the unmapped contexts once those exist.
        },
        // Remove all user-defined mappings for the provided context.
        mapclear: function (ctx) {
            // Partition the existing keymap into user-defined and true defaults.
            var actualLength = defaultKeymap.length,
                origLength = defaultKeymapLength
            var userKeymap = defaultKeymap.slice(0, actualLength - origLength)
            defaultKeymap = defaultKeymap.slice(actualLength - origLength)
            if (ctx) {
                // If a specific context is being cleared, we need to keep mappings
                // from all other contexts.
                for (var i = userKeymap.length - 1; i >= 0; i--) {
                    var mapping = userKeymap[i]
                    if (ctx !== mapping.context) {
                        if (mapping.context) {
                            this._mapCommand(mapping)
                        } else {
                            // `mapping` applies to all contexts so create keymap copies
                            // for each context except the one being cleared.
                            var contexts = ['normal', 'insert', 'visual']
                            for (var j in contexts) {
                                if (contexts[j] !== ctx) {
                                    var newMapping = {}
                                    for (var key in mapping) {
                                        newMapping[key] = mapping[key]
                                    }
                                    newMapping.context = contexts[j]
                                    this._mapCommand(newMapping)
                                }
                            }
                        }
                    }
                }
            }
        },
        // TODO: Expose setOption and getOption as instance methods. Need to decide how to namespace
        // them, or somehow make them work with the existing CodeMirror setOption/getOption API.
        setOption: setOption,
        getOption: getOption,
        defineOption: defineOption,
        defineEx: function (name, prefix, func) {
            if (!prefix) {
                prefix = name
            } else if (name.indexOf(prefix) !== 0) {
                throw new Error(
                    '(Vim.defineEx) "' +
                        prefix +
                        '" is not a prefix of "' +
                        name +
                        '", command not registered'
                )
            }
            exCommands[name] = func
            exCommandDispatcher.commandMap_[prefix] = {
                name: name,
                shortName: prefix,
                type: 'api',
            }
        },
        handleKey: function (cm, key, origin) {
            var command = this.findKey(cm, key, origin)
            if (typeof command === 'function') {
                return command()
            }
        },
        multiSelectHandleKey: multiSelectHandleKey,

        /**
         * This is the outermost function called by CodeMirror, after keys have
         * been mapped to their Vim equivalents.
         *
         * Finds a command based on the key (and cached keys if there is a
         * multi-key sequence). Returns `undefined` if no key is matched, a noop
         * function if a partial match is found (multi-key), and a function to
         * execute the bound command if a a key is matched. The function always
         * returns true.
         */
        findKey: function (cm, key, origin) {
            var vim = maybeInitVimState(cm)
            function handleMacroRecording() {
                var macroModeState = vimGlobalState.macroModeState
                if (macroModeState.isRecording) {
                    if (key == 'q') {
                        macroModeState.exitMacroRecordMode()
                        clearInputState(cm)
                        return true
                    }
                    if (origin != 'mapping') {
                        logKey(macroModeState, key)
                    }
                }
            }
            function handleEsc() {
                if (key == '<Esc>') {
                    if (vim.visualMode) {
                        // Get back to normal mode.
                        exitVisualMode(cm)
                    } else if (vim.insertMode) {
                        // Get back to normal mode.
                        exitInsertMode(cm)
                    } else {
                        // We're already in normal mode. Let '<Esc>' be handled normally.
                        return
                    }
                    clearInputState(cm)
                    return true
                }
            }
            function doKeyToKey(keys) {
                // TODO: prevent infinite recursion.
                var match
                while (keys) {
                    // Pull off one command key, which is either a single character
                    // or a special sequence wrapped in '<' and '>', e.g. '<Space>'.
                    match = /<\w+-.+?>|<\w+>|./.exec(keys)
                    key = match[0]
                    keys = keys.substring(match.index + key.length)
                    vimApi.handleKey(cm, key, 'mapping')
                }
            }

            function handleKeyInsertMode() {
                if (handleEsc()) {
                    return true
                }
                vim.inputState.keyBuffer.push(key)
                var keys = vim.inputState.keyBuffer.join('')
                var keysAreChars = key.length == 1
                var match = commandDispatcher.matchCommand(
                    keys,
                    defaultKeymap,
                    vim.inputState,
                    'insert'
                )
                var changeQueue = vim.inputState.changeQueue

                if (match.type == 'none') {
                    clearInputState(cm)
                    return false
                } else if (match.type == 'partial') {
                    if (lastInsertModeKeyTimer) {
                        window.clearTimeout(lastInsertModeKeyTimer)
                    }
                    lastInsertModeKeyTimer =
                        keysAreChars &&
                        window.setTimeout(function () {
                            if (
                                vim.insertMode &&
                                vim.inputState.keyBuffer.length
                            ) {
                                clearInputState(cm)
                            }
                        }, getOption('insertModeEscKeysTimeout'))
                    if (keysAreChars) {
                        var selections = cm.listSelections()
                        if (
                            !changeQueue ||
                            changeQueue.removed.length != selections.length
                        )
                            changeQueue = vim.inputState.changeQueue =
                                new ChangeQueue()
                        changeQueue.inserted += key
                        for (var i = 0; i < selections.length; i++) {
                            var from = cursorMin(
                                selections[i].anchor,
                                selections[i].head
                            )
                            var to = cursorMax(
                                selections[i].anchor,
                                selections[i].head
                            )
                            var text = cm.getRange(
                                from,
                                cm.state.overwrite ? offsetCursor(to, 0, 1) : to
                            )
                            changeQueue.removed[i] =
                                (changeQueue.removed[i] || '') + text
                        }
                    }
                    return !keysAreChars
                }

                if (lastInsertModeKeyTimer) {
                    window.clearTimeout(lastInsertModeKeyTimer)
                }
                if (match.command && changeQueue) {
                    var selections = cm.listSelections()
                    for (var i = 0; i < selections.length; i++) {
                        var here = selections[i].head
                        cm.replaceRange(
                            changeQueue.removed[i] || '',
                            offsetCursor(here, 0, -changeQueue.inserted.length),
                            here,
                            '+input'
                        )
                    }
                    vimGlobalState.macroModeState.lastInsertModeChanges.changes.pop()
                }
                if (!match.command) clearInputState(cm)
                return match.command
            }

            function handleKeyNonInsertMode() {
                if (handleMacroRecording() || handleEsc()) {
                    return true
                }

                vim.inputState.keyBuffer.push(key)
                var keys = vim.inputState.keyBuffer.join('')
                if (/^[1-9]\d*$/.test(keys)) {
                    return true
                }

                var keysMatcher = /^(\d*)(.*)$/.exec(keys)
                if (!keysMatcher) {
                    clearInputState(cm)
                    return false
                }
                var context = vim.visualMode ? 'visual' : 'normal'
                var mainKey = keysMatcher[2] || keysMatcher[1]
                if (
                    vim.inputState.operatorShortcut &&
                    vim.inputState.operatorShortcut.slice(-1) == mainKey
                ) {
                    // multikey operators act linewise by repeating only the last character
                    mainKey = vim.inputState.operatorShortcut
                }
                var match = commandDispatcher.matchCommand(
                    mainKey,
                    defaultKeymap,
                    vim.inputState,
                    context
                )
                if (match.type == 'none') {
                    clearInputState(cm)
                    return false
                } else if (match.type == 'partial') {
                    return true
                } else if (match.type == 'clear') {
                    clearInputState(cm)
                    return true
                }

                vim.inputState.keyBuffer.length = 0
                keysMatcher = /^(\d*)(.*)$/.exec(keys)
                if (keysMatcher[1] && keysMatcher[1] != '0') {
                    vim.inputState.pushRepeatDigit(keysMatcher[1])
                }
                return match.command
            }

            var command
            if (vim.insertMode) {
                command = handleKeyInsertMode()
            } else {
                command = handleKeyNonInsertMode()
            }
            if (command === false) {
                return !vim.insertMode && key.length === 1
                    ? function () {
                          return true
                      }
                    : undefined
            } else if (command === true) {
                // TODO: Look into using CodeMirror's multi-key handling.
                // Return no-op since we are caching the key. Counts as handled, but
                // don't want act on it just yet.
                return function () {
                    return true
                }
            } else {
                return function () {
                    return cm.operation(function () {
                        cm.curOp.isVimOp = true
                        try {
                            if (command.type == 'keyToKey') {
                                doKeyToKey(command.toKeys)
                            } else {
                                commandDispatcher.processCommand(
                                    cm,
                                    vim,
                                    command
                                )
                            }
                        } catch (e) {
                            // clear VIM state in case it's in a bad state.
                            cm.state.vim = undefined
                            maybeInitVimState(cm)
                            if (!vimApi.suppressErrorLogging) {
                                console['log'](e)
                            }
                            throw e
                        }
                        return true
                    })
                }
            }
        },
        handleEx: function (cm, input) {
            exCommandDispatcher.processCommand(cm, input)
        },

        defineMotion: defineMotion,
        defineAction: defineAction,
        defineOperator: defineOperator,
        mapCommand: mapCommand,
        _mapCommand: _mapCommand,

        defineRegister: defineRegister,

        exitVisualMode: exitVisualMode,
        exitInsertMode: exitInsertMode,
    }

    // Represents the current input state.
    function InputState() {
        this.prefixRepeat = []
        this.motionRepeat = []

        this.operator = null
        this.operatorArgs = null
        this.motion = null
        this.motionArgs = null
        this.keyBuffer = [] // For matching multi-key commands.
        this.registerName = null // Defaults to the unnamed register.
        this.changeQueue = null // For restoring text used by insert mode keybindings
    }
    InputState.prototype.pushRepeatDigit = function (n) {
        if (!this.operator) {
            this.prefixRepeat = this.prefixRepeat.concat(n)
        } else {
            this.motionRepeat = this.motionRepeat.concat(n)
        }
    }
    InputState.prototype.getRepeat = function () {
        var repeat = 0
        if (this.prefixRepeat.length > 0 || this.motionRepeat.length > 0) {
            repeat = 1
            if (this.prefixRepeat.length > 0) {
                repeat *= parseInt(this.prefixRepeat.join(''), 10)
            }
            if (this.motionRepeat.length > 0) {
                repeat *= parseInt(this.motionRepeat.join(''), 10)
            }
        }
        return repeat
    }

    function clearInputState(cm, reason) {
        cm.state.vim.inputState = new InputState()
        CodeMirror.signal(cm, 'vim-command-done', reason)
    }

    function ChangeQueue() {
        this.removed = []
        this.inserted = ''
    }

    /*
     * Register stores information about copy and paste registers.  Besides
     * text, a register must store whether it is linewise (i.e., when it is
     * pasted, should it insert itself into a new line, or should the text be
     * inserted at the cursor position.)
     */
    function Register(text, linewise, blockwise) {
        this.clear()
        this.keyBuffer = [text || '']
        this.insertModeChanges = []
        this.searchQueries = []
        this.linewise = !!linewise
        this.blockwise = !!blockwise
    }
    Register.prototype = {
        setText: function (text, linewise, blockwise) {
            this.keyBuffer = [text || '']
            this.linewise = !!linewise
            this.blockwise = !!blockwise
        },
        pushText: function (text, linewise) {
            // if this register has ever been set to linewise, use linewise.
            if (linewise) {
                if (!this.linewise) {
                    this.keyBuffer.push('\n')
                }
                this.linewise = true
            }
            this.keyBuffer.push(text)
        },
        pushInsertModeChanges: function (changes) {
            this.insertModeChanges.push(createInsertModeChanges(changes))
        },
        pushSearchQuery: function (query) {
            this.searchQueries.push(query)
        },
        clear: function () {
            this.keyBuffer = []
            this.insertModeChanges = []
            this.searchQueries = []
            this.linewise = false
        },
        toString: function () {
            return this.keyBuffer.join('')
        },
    }

    /**
     * Defines an external register.
     *
     * The name should be a single character that will be used to reference the register.
     * The register should support setText, pushText, clear, and toString(). See Register
     * for a reference implementation.
     */
    function defineRegister(name, register) {
        var registers = vimGlobalState.registerController.registers
        if (!name || name.length != 1) {
            throw Error('Register name must be 1 character')
        }
        if (registers[name]) {
            throw Error('Register already defined ' + name)
        }
        registers[name] = register
        validRegisters.push(name)
    }

    /*
     * vim registers allow you to keep many independent copy and paste buffers.
     * See http://usevim.com/2012/04/13/registers/ for an introduction.
     *
     * RegisterController keeps the state of all the registers.  An initial
     * state may be passed in.  The unnamed register '"' will always be
     * overridden.
     */
    function RegisterController(registers) {
        this.registers = registers
        this.unnamedRegister = registers['"'] = new Register()
        registers['.'] = new Register()
        registers[':'] = new Register()
        registers['/'] = new Register()
        registers['+'] = new Register()
    }
    RegisterController.prototype = {
        pushText: function (registerName, operator, text, linewise, blockwise) {
            // The black hole register, "_, means delete/yank to nowhere.
            if (registerName === '_') return
            if (linewise && text.charAt(text.length - 1) !== '\n') {
                text += '\n'
            }
            // Lowercase and uppercase registers refer to the same register.
            // Uppercase just means append.
            var register = this.isValidRegister(registerName)
                ? this.getRegister(registerName)
                : null
            // if no register/an invalid register was specified, things go to the
            // default registers
            if (!register) {
                switch (operator) {
                    case 'yank':
                        // The 0 register contains the text from the most recent yank.
                        this.registers['0'] = new Register(
                            text,
                            linewise,
                            blockwise
                        )
                        break
                    case 'delete':
                    case 'change':
                        if (text.indexOf('\n') == -1) {
                            // Delete less than 1 line. Update the small delete register.
                            this.registers['-'] = new Register(text, linewise)
                        } else {
                            // Shift down the contents of the numbered registers and put the
                            // deleted text into register 1.
                            this.shiftNumericRegisters_()
                            this.registers['1'] = new Register(text, linewise)
                        }
                        break
                }
                // Make sure the unnamed register is set to what just happened
                this.unnamedRegister.setText(text, linewise, blockwise)
                return
            }

            // If we've gotten to this point, we've actually specified a register
            var append = isUpperCase(registerName)
            if (append) {
                register.pushText(text, linewise)
            } else {
                register.setText(text, linewise, blockwise)
            }
            if (registerName === '+') {
                navigator.clipboard.writeText(text)
            }
            // The unnamed register always has the same value as the last used
            // register.
            this.unnamedRegister.setText(register.toString(), linewise)
        },
        // Gets the register named @name.  If one of @name doesn't already exist,
        // create it.  If @name is invalid, return the unnamedRegister.
        getRegister: function (name) {
            if (!this.isValidRegister(name)) {
                return this.unnamedRegister
            }
            name = name.toLowerCase()
            if (!this.registers[name]) {
                this.registers[name] = new Register()
            }
            return this.registers[name]
        },
        isValidRegister: function (name) {
            return name && inArray(name, validRegisters)
        },
        shiftNumericRegisters_: function () {
            for (var i = 9; i >= 2; i--) {
                this.registers[i] = this.getRegister('' + (i - 1))
            }
        },
    }
    function HistoryController() {
        this.historyBuffer = []
        this.iterator = 0
        this.initialPrefix = null
    }
    HistoryController.prototype = {
        // the input argument here acts a user entered prefix for a small time
        // until we start autocompletion in which case it is the autocompleted.
        nextMatch: function (input, up) {
            var historyBuffer = this.historyBuffer
            var dir = up ? -1 : 1
            if (this.initialPrefix === null) this.initialPrefix = input
            for (
                var i = this.iterator + dir;
                up ? i >= 0 : i < historyBuffer.length;
                i += dir
            ) {
                var element = historyBuffer[i]
                for (var j = 0; j <= element.length; j++) {
                    if (this.initialPrefix == element.substring(0, j)) {
                        this.iterator = i
                        return element
                    }
                }
            }
            // should return the user input in case we reach the end of buffer.
            if (i >= historyBuffer.length) {
                this.iterator = historyBuffer.length
                return this.initialPrefix
            }
            // return the last autocompleted query or exCommand as it is.
            if (i < 0) return input
        },
        pushInput: function (input) {
            var index = this.historyBuffer.indexOf(input)
            if (index > -1) this.historyBuffer.splice(index, 1)
            if (input.length) this.historyBuffer.push(input)
        },
        reset: function () {
            this.initialPrefix = null
            this.iterator = this.historyBuffer.length
        },
    }
    var commandDispatcher = {
        matchCommand: function (keys, keyMap, inputState, context) {
            var matches = commandMatches(keys, keyMap, context, inputState)
            if (!matches.full && !matches.partial) {
                return { type: 'none' }
            } else if (!matches.full && matches.partial) {
                return { type: 'partial' }
            }

            var bestMatch
            for (var i = 0; i < matches.full.length; i++) {
                var match = matches.full[i]
                if (!bestMatch) {
                    bestMatch = match
                }
            }
            if (bestMatch.keys.slice(-11) == '<character>') {
                var character = lastChar(keys)
                if (!character || character.length > 1) return { type: 'clear' }
                inputState.selectedCharacter = character
            }
            return { type: 'full', command: bestMatch }
        },
        processCommand: function (cm, vim, command) {
            vim.inputState.repeatOverride = command.repeatOverride
            switch (command.type) {
                case 'motion':
                    this.processMotion(cm, vim, command)
                    break
                case 'operator':
                    this.processOperator(cm, vim, command)
                    break
                case 'operatorMotion':
                    this.processOperatorMotion(cm, vim, command)
                    break
                case 'action':
                    this.processAction(cm, vim, command)
                    break
                case 'search':
                    this.processSearch(cm, vim, command)
                    break
                case 'ex':
                case 'keyToEx':
                    this.processEx(cm, vim, command)
                    break
                default:
                    break
            }
        },
        processMotion: function (cm, vim, command) {
            vim.inputState.motion = command.motion
            vim.inputState.motionArgs = copyArgs(command.motionArgs)
            this.evalInput(cm, vim)
        },
        processOperator: function (cm, vim, command) {
            var inputState = vim.inputState
            if (inputState.operator) {
                if (inputState.operator == command.operator) {
                    // Typing an operator twice like 'dd' makes the operator operate
                    // linewise
                    inputState.motion = 'expandToLine'
                    inputState.motionArgs = { linewise: true }
                    this.evalInput(cm, vim)
                    return
                } else {
                    // 2 different operators in a row doesn't make sense.
                    clearInputState(cm)
                }
            }
            inputState.operator = command.operator
            inputState.operatorArgs = copyArgs(command.operatorArgs)
            if (command.keys.length > 1) {
                inputState.operatorShortcut = command.keys
            }
            if (command.exitVisualBlock) {
                vim.visualBlock = false
                updateCmSelection(cm)
            }
            if (vim.visualMode) {
                // Operating on a selection in visual mode. We don't need a motion.
                this.evalInput(cm, vim)
            }
        },
        processOperatorMotion: function (cm, vim, command) {
            var visualMode = vim.visualMode
            var operatorMotionArgs = copyArgs(command.operatorMotionArgs)
            if (operatorMotionArgs) {
                // Operator motions may have special behavior in visual mode.
                if (visualMode && operatorMotionArgs.visualLine) {
                    vim.visualLine = true
                }
            }
            this.processOperator(cm, vim, command)
            if (!visualMode) {
                this.processMotion(cm, vim, command)
            }
        },
        processAction: function (cm, vim, command) {
            var inputState = vim.inputState
            var repeat = inputState.getRepeat()
            var repeatIsExplicit = !!repeat
            var actionArgs = copyArgs(command.actionArgs) || {}
            if (inputState.selectedCharacter) {
                actionArgs.selectedCharacter = inputState.selectedCharacter
            }
            // Actions may or may not have motions and operators. Do these first.
            if (command.operator) {
                this.processOperator(cm, vim, command)
            }
            if (command.motion) {
                this.processMotion(cm, vim, command)
            }
            if (command.motion || command.operator) {
                this.evalInput(cm, vim)
            }
            actionArgs.repeat = repeat || 1
            actionArgs.repeatIsExplicit = repeatIsExplicit
            actionArgs.registerName = inputState.registerName
            clearInputState(cm)
            vim.lastMotion = null
            if (command.isEdit) {
                this.recordLastEdit(vim, inputState, command)
            }
            actions[command.action](cm, actionArgs, vim)
        },
        processSearch: function (cm, vim, command) {
            if (!cm.getSearchCursor) {
                // Search depends on SearchCursor.
                return
            }
            var forward = command.searchArgs.forward
            var wholeWordOnly = command.searchArgs.wholeWordOnly
            getSearchState(cm).setReversed(!forward)
            var promptPrefix = forward ? '/' : '?'
            var originalQuery = getSearchState(cm).getQuery()
            var originalScrollPos = cm.getScrollInfo()
            function handleQuery(query, ignoreCase, smartCase) {
                vimGlobalState.searchHistoryController.pushInput(query)
                vimGlobalState.searchHistoryController.reset()
                try {
                    updateSearchQuery(cm, query, ignoreCase, smartCase)
                } catch (e) {
                    showConfirm(cm, 'Invalid regex: ' + query)
                    clearInputState(cm)
                    return
                }
                commandDispatcher.processMotion(cm, vim, {
                    type: 'motion',
                    motion: 'findNext',
                    motionArgs: {
                        forward: true,
                        toJumplist: command.searchArgs.toJumplist,
                    },
                })
            }
            function onPromptClose(query) {
                cm.scrollTo(originalScrollPos.left, originalScrollPos.top)
                handleQuery(
                    query,
                    true /** ignoreCase */,
                    true /** smartCase */
                )
                var macroModeState = vimGlobalState.macroModeState
                if (macroModeState.isRecording) {
                    logSearchQuery(macroModeState, query)
                }
            }
            function onPromptKeyUp(e, query, close) {
                var keyName = CodeMirror.keyName(e),
                    up,
                    offset
                if (keyName == 'Up' || keyName == 'Down') {
                    up = keyName == 'Up' ? true : false
                    offset = e.target ? e.target.selectionEnd : 0
                    query =
                        vimGlobalState.searchHistoryController.nextMatch(
                            query,
                            up
                        ) || ''
                    close(query)
                    if (offset && e.target)
                        e.target.selectionEnd = e.target.selectionStart =
                            Math.min(offset, e.target.value.length)
                } else {
                    if (
                        keyName != 'Left' &&
                        keyName != 'Right' &&
                        keyName != 'Ctrl' &&
                        keyName != 'Alt' &&
                        keyName != 'Shift'
                    )
                        vimGlobalState.searchHistoryController.reset()
                }
                var parsedQuery
                try {
                    parsedQuery = updateSearchQuery(
                        cm,
                        query,
                        true /** ignoreCase */,
                        true /** smartCase */
                    )
                } catch (e) {
                    // Swallow bad regexes for incremental search.
                }
                if (parsedQuery) {
                    cm.scrollIntoView(findNext(cm, !forward, parsedQuery), 30)
                } else {
                    clearSearchHighlight(cm)
                    cm.scrollTo(originalScrollPos.left, originalScrollPos.top)
                }
            }
            function onPromptKeyDown(e, query, close) {
                var keyName = CodeMirror.keyName(e)
                if (
                    keyName == 'Esc' ||
                    keyName == 'Ctrl-C' ||
                    keyName == 'Ctrl-[' ||
                    (keyName == 'Backspace' && query == '')
                ) {
                    vimGlobalState.searchHistoryController.pushInput(query)
                    vimGlobalState.searchHistoryController.reset()
                    updateSearchQuery(cm, originalQuery)
                    clearSearchHighlight(cm)
                    cm.scrollTo(originalScrollPos.left, originalScrollPos.top)
                    CodeMirror.e_stop(e)
                    clearInputState(cm)
                    close()
                    cm.focus()
                } else if (keyName == 'Up' || keyName == 'Down') {
                    CodeMirror.e_stop(e)
                } else if (keyName == 'Ctrl-U') {
                    // Ctrl-U clears input.
                    CodeMirror.e_stop(e)
                    close('')
                }
            }
            switch (command.searchArgs.querySrc) {
                case 'prompt':
                    var macroModeState = vimGlobalState.macroModeState
                    if (macroModeState.isPlaying) {
                        var query = macroModeState.replaySearchQueries.shift()
                        handleQuery(
                            query,
                            true /** ignoreCase */,
                            false /** smartCase */
                        )
                    } else {
                        showPrompt(cm, {
                            onClose: onPromptClose,
                            prefix: promptPrefix,
                            desc: '(JavaScript regexp)',
                            onKeyUp: onPromptKeyUp,
                            onKeyDown: onPromptKeyDown,
                        })
                    }
                    break
                case 'wordUnderCursor':
                    var word = expandWordUnderCursor(
                        cm,
                        false /** inclusive */,
                        true /** forward */,
                        false /** bigWord */,
                        true /** noSymbol */
                    )
                    var isKeyword = true
                    if (!word) {
                        word = expandWordUnderCursor(
                            cm,
                            false /** inclusive */,
                            true /** forward */,
                            false /** bigWord */,
                            false /** noSymbol */
                        )
                        isKeyword = false
                    }
                    if (!word) {
                        return
                    }
                    var query = cm
                        .getLine(word.start.line)
                        .substring(word.start.ch, word.end.ch)
                    if (isKeyword && wholeWordOnly) {
                        query = '\\b' + query + '\\b'
                    } else {
                        query = escapeRegex(query)
                    }

                    // cachedCursor is used to save the old position of the cursor
                    // when * or # causes vim to seek for the nearest word and shift
                    // the cursor before entering the motion.
                    vimGlobalState.jumpList.cachedCursor = cm.getCursor()
                    cm.setCursor(word.start)

                    handleQuery(
                        query,
                        true /** ignoreCase */,
                        false /** smartCase */
                    )
                    break
            }
        },
        processEx: function (cm, vim, command) {
            function onPromptClose(input) {
                // Give the prompt some time to close so that if processCommand shows
                // an error, the elements don't overlap.
                vimGlobalState.exCommandHistoryController.pushInput(input)
                vimGlobalState.exCommandHistoryController.reset()
                exCommandDispatcher.processCommand(cm, input)
                if (cm.state.vim) clearInputState(cm)
            }
            function onPromptKeyDown(e, input, close) {
                var keyName = CodeMirror.keyName(e),
                    up,
                    offset
                if (
                    keyName == 'Esc' ||
                    keyName == 'Ctrl-C' ||
                    keyName == 'Ctrl-[' ||
                    (keyName == 'Backspace' && input == '')
                ) {
                    vimGlobalState.exCommandHistoryController.pushInput(input)
                    vimGlobalState.exCommandHistoryController.reset()
                    CodeMirror.e_stop(e)
                    clearInputState(cm)
                    close()
                    cm.focus()
                }
                if (keyName == 'Up' || keyName == 'Down') {
                    CodeMirror.e_stop(e)
                    up = keyName == 'Up' ? true : false
                    offset = e.target ? e.target.selectionEnd : 0
                    input =
                        vimGlobalState.exCommandHistoryController.nextMatch(
                            input,
                            up
                        ) || ''
                    close(input)
                    if (offset && e.target)
                        e.target.selectionEnd = e.target.selectionStart =
                            Math.min(offset, e.target.value.length)
                } else if (keyName == 'Ctrl-U') {
                    // Ctrl-U clears input.
                    CodeMirror.e_stop(e)
                    close('')
                } else {
                    if (
                        keyName != 'Left' &&
                        keyName != 'Right' &&
                        keyName != 'Ctrl' &&
                        keyName != 'Alt' &&
                        keyName != 'Shift'
                    )
                        vimGlobalState.exCommandHistoryController.reset()
                }
            }
            if (command.type == 'keyToEx') {
                // Handle user defined Ex to Ex mappings
                exCommandDispatcher.processCommand(cm, command.exArgs.input)
            } else {
                if (vim.visualMode) {
                    showPrompt(cm, {
                        onClose: onPromptClose,
                        prefix: ':',
                        value: "'<,'>",
                        onKeyDown: onPromptKeyDown,
                        selectValueOnOpen: false,
                    })
                } else {
                    showPrompt(cm, {
                        onClose: onPromptClose,
                        prefix: ':',
                        onKeyDown: onPromptKeyDown,
                    })
                }
            }
        },
        evalInput: function (cm, vim) {
            // If the motion command is set, execute both the operator and motion.
            // Otherwise return.
            var inputState = vim.inputState
            var motion = inputState.motion
            var motionArgs = inputState.motionArgs || {}
            var operator = inputState.operator
            var operatorArgs = inputState.operatorArgs || {}
            var registerName = inputState.registerName
            var sel = vim.sel
            // TODO: Make sure cm and vim selections are identical outside visual mode.
            var origHead = copyCursor(
                vim.visualMode
                    ? clipCursorToContent(cm, sel.head)
                    : cm.getCursor('head')
            )
            var origAnchor = copyCursor(
                vim.visualMode
                    ? clipCursorToContent(cm, sel.anchor)
                    : cm.getCursor('anchor')
            )
            var oldHead = copyCursor(origHead)
            var oldAnchor = copyCursor(origAnchor)
            var newHead, newAnchor
            var repeat
            if (operator) {
                this.recordLastEdit(vim, inputState)
            }
            if (inputState.repeatOverride !== undefined) {
                // If repeatOverride is specified, that takes precedence over the
                // input state's repeat. Used by Ex mode and can be user defined.
                repeat = inputState.repeatOverride
            } else {
                repeat = inputState.getRepeat()
            }
            if (repeat > 0 && motionArgs.explicitRepeat) {
                motionArgs.repeatIsExplicit = true
            } else if (
                motionArgs.noRepeat ||
                (!motionArgs.explicitRepeat && repeat === 0)
            ) {
                repeat = 1
                motionArgs.repeatIsExplicit = false
            }
            if (inputState.selectedCharacter) {
                // If there is a character input, stick it in all of the arg arrays.
                motionArgs.selectedCharacter = operatorArgs.selectedCharacter =
                    inputState.selectedCharacter
            }
            motionArgs.repeat = repeat
            clearInputState(cm)
            if (motion) {
                var motionResult = motions[motion](
                    cm,
                    origHead,
                    motionArgs,
                    vim,
                    inputState
                )
                vim.lastMotion = motions[motion]
                if (!motionResult) {
                    return
                }
                if (motionArgs.toJumplist) {
                    var jumpList = vimGlobalState.jumpList
                    // if the current motion is # or *, use cachedCursor
                    var cachedCursor = jumpList.cachedCursor
                    if (cachedCursor) {
                        recordJumpPosition(cm, cachedCursor, motionResult)
                        delete jumpList.cachedCursor
                    } else {
                        recordJumpPosition(cm, origHead, motionResult)
                    }
                }
                if (motionResult instanceof Array) {
                    newAnchor = motionResult[0]
                    newHead = motionResult[1]
                } else {
                    newHead = motionResult
                }
                // TODO: Handle null returns from motion commands better.
                if (!newHead) {
                    newHead = copyCursor(origHead)
                }
                if (vim.visualMode) {
                    if (!(vim.visualBlock && newHead.ch === Infinity)) {
                        newHead = clipCursorToContent(cm, newHead, oldHead)
                    }
                    if (newAnchor) {
                        newAnchor = clipCursorToContent(cm, newAnchor)
                    }
                    newAnchor = newAnchor || oldAnchor
                    sel.anchor = newAnchor
                    sel.head = newHead
                    updateCmSelection(cm)
                    updateMark(
                        cm,
                        vim,
                        '<',
                        cursorIsBefore(newAnchor, newHead) ? newAnchor : newHead
                    )
                    updateMark(
                        cm,
                        vim,
                        '>',
                        cursorIsBefore(newAnchor, newHead) ? newHead : newAnchor
                    )
                } else if (!operator) {
                    newHead = clipCursorToContent(cm, newHead, oldHead)
                    cm.setCursor(newHead.line, newHead.ch)
                }
            }
            if (operator) {
                if (operatorArgs.lastSel) {
                    // Replaying a visual mode operation
                    newAnchor = oldAnchor
                    var lastSel = operatorArgs.lastSel
                    var lineOffset = Math.abs(
                        lastSel.head.line - lastSel.anchor.line
                    )
                    var chOffset = Math.abs(lastSel.head.ch - lastSel.anchor.ch)
                    if (lastSel.visualLine) {
                        // Linewise Visual mode: The same number of lines.
                        newHead = new Pos(
                            oldAnchor.line + lineOffset,
                            oldAnchor.ch
                        )
                    } else if (lastSel.visualBlock) {
                        // Blockwise Visual mode: The same number of lines and columns.
                        newHead = new Pos(
                            oldAnchor.line + lineOffset,
                            oldAnchor.ch + chOffset
                        )
                    } else if (lastSel.head.line == lastSel.anchor.line) {
                        // Normal Visual mode within one line: The same number of characters.
                        newHead = new Pos(
                            oldAnchor.line,
                            oldAnchor.ch + chOffset
                        )
                    } else {
                        // Normal Visual mode with several lines: The same number of lines, in the
                        // last line the same number of characters as in the last line the last time.
                        newHead = new Pos(
                            oldAnchor.line + lineOffset,
                            oldAnchor.ch
                        )
                    }
                    vim.visualMode = true
                    vim.visualLine = lastSel.visualLine
                    vim.visualBlock = lastSel.visualBlock
                    sel = vim.sel = {
                        anchor: newAnchor,
                        head: newHead,
                    }
                    updateCmSelection(cm)
                } else if (vim.visualMode) {
                    operatorArgs.lastSel = {
                        anchor: copyCursor(sel.anchor),
                        head: copyCursor(sel.head),
                        visualBlock: vim.visualBlock,
                        visualLine: vim.visualLine,
                    }
                }
                var curStart, curEnd, linewise, mode
                var cmSel
                if (vim.visualMode) {
                    // Init visual op
                    curStart = cursorMin(sel.head, sel.anchor)
                    curEnd = cursorMax(sel.head, sel.anchor)
                    linewise = vim.visualLine || operatorArgs.linewise
                    mode = vim.visualBlock
                        ? 'block'
                        : linewise
                        ? 'line'
                        : 'char'
                    var newPositions = updateSelectionForSurrogateCharacters(
                        cm,
                        curStart,
                        curEnd
                    )
                    cmSel = makeCmSelection(
                        cm,
                        {
                            anchor: newPositions.start,
                            head: newPositions.end,
                        },
                        mode
                    )
                    if (linewise) {
                        var ranges = cmSel.ranges
                        if (mode == 'block') {
                            // Linewise operators in visual block mode extend to end of line
                            for (var i = 0; i < ranges.length; i++) {
                                ranges[i].head.ch = lineLength(
                                    cm,
                                    ranges[i].head.line
                                )
                            }
                        } else if (mode == 'line') {
                            ranges[0].head = new Pos(ranges[0].head.line + 1, 0)
                        }
                    }
                } else {
                    // Init motion op
                    curStart = copyCursor(newAnchor || oldAnchor)
                    curEnd = copyCursor(newHead || oldHead)
                    if (cursorIsBefore(curEnd, curStart)) {
                        var tmp = curStart
                        curStart = curEnd
                        curEnd = tmp
                    }
                    linewise = motionArgs.linewise || operatorArgs.linewise
                    if (linewise) {
                        // Expand selection to entire line.
                        expandSelectionToLine(cm, curStart, curEnd)
                    } else if (motionArgs.forward) {
                        // Clip to trailing newlines only if the motion goes forward.
                        clipToLine(cm, curStart, curEnd)
                    }
                    mode = 'char'
                    var exclusive = !motionArgs.inclusive || linewise
                    var newPositions = updateSelectionForSurrogateCharacters(
                        cm,
                        curStart,
                        curEnd
                    )
                    cmSel = makeCmSelection(
                        cm,
                        {
                            anchor: newPositions.start,
                            head: newPositions.end,
                        },
                        mode,
                        exclusive
                    )
                }
                cm.setSelections(cmSel.ranges, cmSel.primary)
                vim.lastMotion = null
                operatorArgs.repeat = repeat // For indent in visual mode.
                operatorArgs.registerName = registerName
                // Keep track of linewise as it affects how paste and change behave.
                operatorArgs.linewise = linewise
                var operatorMoveTo = operators[operator](
                    cm,
                    operatorArgs,
                    cmSel.ranges,
                    oldAnchor,
                    newHead
                )
                if (vim.visualMode) {
                    exitVisualMode(cm, operatorMoveTo != null)
                }
                if (operatorMoveTo) {
                    cm.setCursor(operatorMoveTo)
                }
            }
        },
        recordLastEdit: function (vim, inputState, actionCommand) {
            var macroModeState = vimGlobalState.macroModeState
            if (macroModeState.isPlaying) {
                return
            }
            vim.lastEditInputState = inputState
            vim.lastEditActionCommand = actionCommand
            macroModeState.lastInsertModeChanges.changes = []
            macroModeState.lastInsertModeChanges.expectCursorActivityForChange = false
            macroModeState.lastInsertModeChanges.visualBlock = vim.visualBlock
                ? vim.sel.head.line - vim.sel.anchor.line
                : 0
        },
    }

    /**
     * typedef {Object{line:number,ch:number}} Cursor An object containing the
     *     position of the cursor.
     */
    // All of the functions below return Cursor objects.
    var motions = {
        moveToTopLine: function (cm, _head, motionArgs) {
            var line = getUserVisibleLines(cm).top + motionArgs.repeat - 1
            return new Pos(
                line,
                findFirstNonWhiteSpaceCharacter(cm.getLine(line))
            )
        },
        moveToMiddleLine: function (cm) {
            var range = getUserVisibleLines(cm)
            var line = Math.floor((range.top + range.bottom) * 0.5)
            return new Pos(
                line,
                findFirstNonWhiteSpaceCharacter(cm.getLine(line))
            )
        },
        moveToBottomLine: function (cm, _head, motionArgs) {
            var line = getUserVisibleLines(cm).bottom - motionArgs.repeat + 1
            return new Pos(
                line,
                findFirstNonWhiteSpaceCharacter(cm.getLine(line))
            )
        },
        expandToLine: function (_cm, head, motionArgs) {
            // Expands forward to end of line, and then to next line if repeat is
            // >1. Does not handle backward motion!
            var cur = head
            return new Pos(cur.line + motionArgs.repeat - 1, Infinity)
        },
        findNext: function (cm, _head, motionArgs) {
            var state = getSearchState(cm)
            var query = state.getQuery()
            if (!query) {
                return
            }
            var prev = !motionArgs.forward
            // If search is initiated with ? instead of /, negate direction.
            prev = state.isReversed() ? !prev : prev
            highlightSearchMatches(cm, query)
            return findNext(cm, prev /** prev */, query, motionArgs.repeat)
        },
        /**
         * Find and select the next occurrence of the search query. If the cursor is currently
         * within a match, then find and select the current match. Otherwise, find the next occurrence in the
         * appropriate direction.
         *
         * This differs from `findNext` in the following ways:
         *
         * 1. Instead of only returning the "from", this returns a "from", "to" range.
         * 2. If the cursor is currently inside a search match, this selects the current match
         *    instead of the next match.
         * 3. If there is no associated operator, this will turn on visual mode.
         */
        findAndSelectNextInclusive: function (
            cm,
            _head,
            motionArgs,
            vim,
            prevInputState
        ) {
            var state = getSearchState(cm)
            var query = state.getQuery()

            if (!query) {
                return
            }

            var prev = !motionArgs.forward
            prev = state.isReversed() ? !prev : prev

            // next: [from, to] | null
            var next = findNextFromAndToInclusive(
                cm,
                prev,
                query,
                motionArgs.repeat,
                vim
            )

            // No matches.
            if (!next) {
                return
            }

            // If there's an operator that will be executed, return the selection.
            if (prevInputState.operator) {
                return next
            }

            // At this point, we know that there is no accompanying operator -- let's
            // deal with visual mode in order to select an appropriate match.

            var from = next[0]
            // For whatever reason, when we use the "to" as returned by searchcursor.js directly,
            // the resulting selection is extended by 1 char. Let's shrink it so that only the
            // match is selected.
            var to = new Pos(next[1].line, next[1].ch - 1)

            if (vim.visualMode) {
                // If we were in visualLine or visualBlock mode, get out of it.
                if (vim.visualLine || vim.visualBlock) {
                    vim.visualLine = false
                    vim.visualBlock = false
                    CodeMirror.signal(cm, 'vim-mode-change', {
                        mode: 'visual',
                        subMode: '',
                    })
                }

                // If we're currently in visual mode, we should extend the selection to include
                // the search result.
                var anchor = vim.sel.anchor
                if (anchor) {
                    if (state.isReversed()) {
                        if (motionArgs.forward) {
                            return [anchor, from]
                        }

                        return [anchor, to]
                    } else {
                        if (motionArgs.forward) {
                            return [anchor, to]
                        }

                        return [anchor, from]
                    }
                }
            } else {
                // Let's turn visual mode on.
                vim.visualMode = true
                vim.visualLine = false
                vim.visualBlock = false
                CodeMirror.signal(cm, 'vim-mode-change', {
                    mode: 'visual',
                    subMode: '',
                })
            }

            return prev ? [to, from] : [from, to]
        },
        goToMark: function (cm, _head, motionArgs, vim) {
            var pos = getMarkPos(cm, vim, motionArgs.selectedCharacter)
            if (pos) {
                return motionArgs.linewise
                    ? {
                          line: pos.line,
                          ch: findFirstNonWhiteSpaceCharacter(
                              cm.getLine(pos.line)
                          ),
                      }
                    : pos
            }
            return null
        },
        moveToOtherHighlightedEnd: function (cm, _head, motionArgs, vim) {
            if (vim.visualBlock && motionArgs.sameLine) {
                var sel = vim.sel
                return [
                    clipCursorToContent(
                        cm,
                        new Pos(sel.anchor.line, sel.head.ch)
                    ),
                    clipCursorToContent(
                        cm,
                        new Pos(sel.head.line, sel.anchor.ch)
                    ),
                ]
            } else {
                return [vim.sel.head, vim.sel.anchor]
            }
        },
        jumpToMark: function (cm, head, motionArgs, vim) {
            var best = head
            for (var i = 0; i < motionArgs.repeat; i++) {
                var cursor = best
                for (var key in vim.marks) {
                    if (!isLowerCase(key)) {
                        continue
                    }
                    var mark = vim.marks[key].find()
                    var isWrongDirection = motionArgs.forward
                        ? cursorIsBefore(mark, cursor)
                        : cursorIsBefore(cursor, mark)

                    if (isWrongDirection) {
                        continue
                    }
                    if (motionArgs.linewise && mark.line == cursor.line) {
                        continue
                    }

                    var equal = cursorEqual(cursor, best)
                    var between = motionArgs.forward
                        ? cursorIsBetween(cursor, mark, best)
                        : cursorIsBetween(best, mark, cursor)

                    if (equal || between) {
                        best = mark
                    }
                }
            }

            if (motionArgs.linewise) {
                // Vim places the cursor on the first non-whitespace character of
                // the line if there is one, else it places the cursor at the end
                // of the line, regardless of whether a mark was found.
                best = new Pos(
                    best.line,
                    findFirstNonWhiteSpaceCharacter(cm.getLine(best.line))
                )
            }
            return best
        },
        moveByCharacters: function (_cm, head, motionArgs) {
            var cur = head
            var repeat = motionArgs.repeat
            var ch = motionArgs.forward ? cur.ch + repeat : cur.ch - repeat
            return new Pos(cur.line, ch)
        },
        moveByLines: function (cm, head, motionArgs, vim) {
            var cur = head
            var endCh = cur.ch
            // Depending what our last motion was, we may want to do different
            // things. If our last motion was moving vertically, we want to
            // preserve the HPos from our last horizontal move.  If our last motion
            // was going to the end of a line, moving vertically we should go to
            // the end of the line, etc.
            switch (vim.lastMotion) {
                case this.moveByLines:
                case this.moveByDisplayLines:
                case this.moveByScroll:
                case this.moveToColumn:
                case this.moveToEol:
                    endCh = vim.lastHPos
                    break
                default:
                    vim.lastHPos = endCh
            }
            var repeat = motionArgs.repeat + (motionArgs.repeatOffset || 0)
            var line = motionArgs.forward
                ? cur.line + repeat
                : cur.line - repeat
            var first = cm.firstLine()
            var last = cm.lastLine()
            var posV = cm.findPosV(
                cur,
                motionArgs.forward ? repeat : -repeat,
                'line',
                vim.lastHSPos
            )
            var hasMarkedText = motionArgs.forward
                ? posV.line > line
                : posV.line < line
            if (hasMarkedText) {
                line = posV.line
                endCh = posV.ch
            }
            // Vim go to line begin or line end when cursor at first/last line and
            // move to previous/next line is triggered.
            if (line < first && cur.line == first) {
                return this.moveToStartOfLine(cm, head, motionArgs, vim)
            } else if (line > last && cur.line == last) {
                return moveToEol(cm, head, motionArgs, vim, true)
            }
            if (motionArgs.toFirstChar) {
                endCh = findFirstNonWhiteSpaceCharacter(cm.getLine(line))
                vim.lastHPos = endCh
            }
            vim.lastHSPos = cm.charCoords(new Pos(line, endCh), 'div').left
            return new Pos(line, endCh)
        },
        moveByDisplayLines: function (cm, head, motionArgs, vim) {
            var cur = head
            switch (vim.lastMotion) {
                case this.moveByDisplayLines:
                case this.moveByScroll:
                case this.moveByLines:
                case this.moveToColumn:
                case this.moveToEol:
                    break
                default:
                    vim.lastHSPos = cm.charCoords(cur, 'div').left
            }
            var repeat = motionArgs.repeat
            var res = cm.findPosV(
                cur,
                motionArgs.forward ? repeat : -repeat,
                'line',
                vim.lastHSPos
            )
            if (res.hitSide) {
                if (motionArgs.forward) {
                    var lastCharCoords = cm.charCoords(res, 'div')
                    var goalCoords = {
                        top: lastCharCoords.top + 8,
                        left: vim.lastHSPos,
                    }
                    var res = cm.coordsChar(goalCoords, 'div')
                } else {
                    var resCoords = cm.charCoords(
                        new Pos(cm.firstLine(), 0),
                        'div'
                    )
                    resCoords.left = vim.lastHSPos
                    res = cm.coordsChar(resCoords, 'div')
                }
            }
            vim.lastHPos = res.ch
            return res
        },
        moveByPage: function (cm, head, motionArgs) {
            // CodeMirror only exposes functions that move the cursor page down, so
            // doing this bad hack to move the cursor and move it back. evalInput
            // will move the cursor to where it should be in the end.
            var curStart = head
            var repeat = motionArgs.repeat
            return cm.findPosV(
                curStart,
                motionArgs.forward ? repeat : -repeat,
                'page'
            )
        },
        moveByParagraph: function (cm, head, motionArgs) {
            var dir = motionArgs.forward ? 1 : -1
            return findParagraph(cm, head, motionArgs.repeat, dir)
        },
        moveBySentence: function (cm, head, motionArgs) {
            var dir = motionArgs.forward ? 1 : -1
            return findSentence(cm, head, motionArgs.repeat, dir)
        },
        moveByScroll: function (cm, head, motionArgs, vim) {
            var scrollbox = cm.getScrollInfo()
            var curEnd = null
            var repeat = motionArgs.repeat
            if (!repeat) {
                repeat = scrollbox.clientHeight / (2 * cm.defaultTextHeight())
            }
            var orig = cm.charCoords(head, 'local')
            motionArgs.repeat = repeat
            curEnd = motions.moveByDisplayLines(cm, head, motionArgs, vim)
            if (!curEnd) {
                return null
            }
            var dest = cm.charCoords(curEnd, 'local')
            cm.scrollTo(null, scrollbox.top + dest.top - orig.top)
            return curEnd
        },
        moveByWords: function (cm, head, motionArgs) {
            return moveToWord(
                cm,
                head,
                motionArgs.repeat,
                !!motionArgs.forward,
                !!motionArgs.wordEnd,
                !!motionArgs.bigWord
            )
        },
        moveTillCharacter: function (cm, _head, motionArgs) {
            var repeat = motionArgs.repeat
            var curEnd = moveToCharacter(
                cm,
                repeat,
                motionArgs.forward,
                motionArgs.selectedCharacter
            )
            var increment = motionArgs.forward ? -1 : 1
            recordLastCharacterSearch(increment, motionArgs)
            if (!curEnd) return null
            curEnd.ch += increment
            return curEnd
        },
        moveToCharacter: function (cm, head, motionArgs) {
            var repeat = motionArgs.repeat
            recordLastCharacterSearch(0, motionArgs)
            return (
                moveToCharacter(
                    cm,
                    repeat,
                    motionArgs.forward,
                    motionArgs.selectedCharacter
                ) || head
            )
        },
        moveToSymbol: function (cm, head, motionArgs) {
            var repeat = motionArgs.repeat
            return (
                findSymbol(
                    cm,
                    repeat,
                    motionArgs.forward,
                    motionArgs.selectedCharacter
                ) || head
            )
        },
        moveToColumn: function (cm, head, motionArgs, vim) {
            var repeat = motionArgs.repeat
            // repeat is equivalent to which column we want to move to!
            vim.lastHPos = repeat - 1
            vim.lastHSPos = cm.charCoords(head, 'div').left
            return moveToColumn(cm, repeat)
        },
        moveToEol: function (cm, head, motionArgs, vim) {
            return moveToEol(cm, head, motionArgs, vim, false)
        },
        moveToFirstNonWhiteSpaceCharacter: function (cm, head) {
            // Go to the start of the line where the text begins, or the end for
            // whitespace-only lines
            var cursor = head
            return new Pos(
                cursor.line,
                findFirstNonWhiteSpaceCharacter(cm.getLine(cursor.line))
            )
        },
        moveToMatchedSymbol: function (cm, head) {
            var cursor = head
            var line = cursor.line
            var ch = cursor.ch
            var lineText = cm.getLine(line)
            var symbol
            for (; ch < lineText.length; ch++) {
                symbol = lineText.charAt(ch)
                if (symbol && isMatchableSymbol(symbol)) {
                    var style = cm.getTokenTypeAt(new Pos(line, ch + 1))
                    if (style !== 'string' && style !== 'comment') {
                        break
                    }
                }
            }
            if (ch < lineText.length) {
                // Only include angle brackets in analysis if they are being matched.
                var re = ch === '<' || ch === '>' ? /[(){}[\]<>]/ : /[(){}[\]]/
                var matched = cm.findMatchingBracket(new Pos(line, ch), {
                    bracketRegex: re,
                })
                return matched.to
            } else {
                return cursor
            }
        },
        moveToStartOfLine: function (_cm, head) {
            return new Pos(head.line, 0)
        },
        moveToLineOrEdgeOfDocument: function (cm, _head, motionArgs) {
            var lineNum = motionArgs.forward ? cm.lastLine() : cm.firstLine()
            if (motionArgs.repeatIsExplicit) {
                lineNum = motionArgs.repeat - cm.getOption('firstLineNumber')
            }
            return new Pos(
                lineNum,
                findFirstNonWhiteSpaceCharacter(cm.getLine(lineNum))
            )
        },
        moveToStartOfDisplayLine: function (cm) {
            cm.execCommand('goLineLeft')
            return cm.getCursor()
        },
        moveToEndOfDisplayLine: function (cm) {
            cm.execCommand('goLineRight')
            var head = cm.getCursor()
            if (head.sticky == 'before') head.ch--
            return head
        },
        textObjectManipulation: function (cm, head, motionArgs, vim) {
            // TODO: lots of possible exceptions that can be thrown here. Try da(
            //     outside of a () block.
            var mirroredPairs = {
                '(': ')',
                ')': '(',
                '{': '}',
                '}': '{',
                '[': ']',
                ']': '[',
                '<': '>',
                '>': '<',
            }
            var selfPaired = { "'": true, '"': true, '`': true }

            var character = motionArgs.selectedCharacter
            // 'b' refers to  '()' block.
            // 'B' refers to  '{}' block.
            if (character == 'b') {
                character = '('
            } else if (character == 'B') {
                character = '{'
            }

            // Inclusive is the difference between a and i
            // TODO: Instead of using the additional text object map to perform text
            //     object operations, merge the map into the defaultKeyMap and use
            //     motionArgs to define behavior. Define separate entries for 'aw',
            //     'iw', 'a[', 'i[', etc.
            var inclusive = !motionArgs.textObjectInner

            var tmp
            if (mirroredPairs[character]) {
                tmp = selectCompanionObject(cm, head, character, inclusive)
            } else if (selfPaired[character]) {
                tmp = findBeginningAndEnd(cm, head, character, inclusive)
            } else if (character === 'W') {
                tmp = expandWordUnderCursor(
                    cm,
                    inclusive,
                    true /** forward */,
                    true /** bigWord */
                )
            } else if (character === 'w') {
                tmp = expandWordUnderCursor(
                    cm,
                    inclusive,
                    true /** forward */,
                    false /** bigWord */
                )
            } else if (character === 'p') {
                tmp = findParagraph(cm, head, motionArgs.repeat, 0, inclusive)
                motionArgs.linewise = true
                if (vim.visualMode) {
                    if (!vim.visualLine) {
                        vim.visualLine = true
                    }
                } else {
                    var operatorArgs = vim.inputState.operatorArgs
                    if (operatorArgs) {
                        operatorArgs.linewise = true
                    }
                    tmp.end.line--
                }
            } else if (character === 't') {
                tmp = expandTagUnderCursor(cm, head, inclusive)
            } else if (character === 's') {
                // account for cursor on end of sentence symbol
                var content = cm.getLine(head.line)
                if (head.ch > 0 && isEndOfSentenceSymbol(content[head.ch])) {
                    head.ch -= 1
                }
                var end = getSentence(cm, head, motionArgs.repeat, 1, inclusive)
                var start = getSentence(
                    cm,
                    head,
                    motionArgs.repeat,
                    -1,
                    inclusive
                )
                // closer vim behaviour, 'a' only takes the space after the sentence if there is one before and after
                if (
                    isWhiteSpaceString(cm.getLine(start.line)[start.ch]) &&
                    isWhiteSpaceString(cm.getLine(end.line)[end.ch - 1])
                ) {
                    start = { line: start.line, ch: start.ch + 1 }
                }
                tmp = { start: start, end: end }
            } else {
                // No text object defined for this, don't move.
                return null
            }

            if (!cm.state.vim.visualMode) {
                return [tmp.start, tmp.end]
            } else {
                return expandSelection(cm, tmp.start, tmp.end)
            }
        },

        repeatLastCharacterSearch: function (cm, head, motionArgs) {
            var lastSearch = vimGlobalState.lastCharacterSearch
            var repeat = motionArgs.repeat
            var forward = motionArgs.forward === lastSearch.forward
            var increment = (lastSearch.increment ? 1 : 0) * (forward ? -1 : 1)
            cm.moveH(-increment, 'char')
            motionArgs.inclusive = forward ? true : false
            var curEnd = moveToCharacter(
                cm,
                repeat,
                forward,
                lastSearch.selectedCharacter
            )
            if (!curEnd) {
                cm.moveH(increment, 'char')
                return head
            }
            curEnd.ch += increment
            return curEnd
        },
    }

    function defineMotion(name, fn) {
        motions[name] = fn
    }

    function fillArray(val, times) {
        var arr = []
        for (var i = 0; i < times; i++) {
            arr.push(val)
        }
        return arr
    }
    /**
     * An operator acts on a text selection. It receives the list of selections
     * as input. The corresponding CodeMirror selection is guaranteed to
     * match the input selection.
     */
    var operators = {
        change: function (cm, args, ranges) {
            var finalHead, text
            var vim = cm.state.vim
            var anchor = ranges[0].anchor,
                head = ranges[0].head
            if (!vim.visualMode) {
                text = cm.getRange(anchor, head)
                var lastState = vim.lastEditInputState || {}
                if (
                    lastState.motion == 'moveByWords' &&
                    !isWhiteSpaceString(text)
                ) {
                    // Exclude trailing whitespace if the range is not all whitespace.
                    var match = /\s+$/.exec(text)
                    if (
                        match &&
                        lastState.motionArgs &&
                        lastState.motionArgs.forward
                    ) {
                        head = offsetCursor(head, 0, -match[0].length)
                        text = text.slice(0, -match[0].length)
                    }
                }
                var prevLineEnd = new Pos(anchor.line - 1, Number.MAX_VALUE)
                var wasLastLine = cm.firstLine() == cm.lastLine()
                if (
                    head.line > cm.lastLine() &&
                    args.linewise &&
                    !wasLastLine
                ) {
                    cm.replaceRange('', prevLineEnd, head)
                } else {
                    cm.replaceRange('', anchor, head)
                }
                if (args.linewise) {
                    // Push the next line back down, if there is a next line.
                    if (!wasLastLine) {
                        cm.setCursor(prevLineEnd)
                        CodeMirror.commands.newlineAndIndent(cm)
                    }
                    // make sure cursor ends up at the end of the line.
                    anchor.ch = Number.MAX_VALUE
                }
                finalHead = anchor
            } else if (args.fullLine) {
                head.ch = Number.MAX_VALUE
                head.line--
                cm.setSelection(anchor, head)
                text = cm.getSelection()
                cm.replaceSelection('')
                finalHead = anchor
            } else {
                text = cm.getSelection()
                var replacement = fillArray('', ranges.length)
                cm.replaceSelections(replacement)
                finalHead = cursorMin(ranges[0].head, ranges[0].anchor)
            }
            vimGlobalState.registerController.pushText(
                args.registerName,
                'change',
                text,
                args.linewise,
                ranges.length > 1
            )
            actions.enterInsertMode(cm, { head: finalHead }, cm.state.vim)
        },
        // delete is a javascript keyword.
        delete: function (cm, args, ranges) {
            var finalHead, text
            var vim = cm.state.vim
            if (!vim.visualBlock) {
                var anchor = ranges[0].anchor,
                    head = ranges[0].head
                if (
                    args.linewise &&
                    head.line != cm.firstLine() &&
                    anchor.line == cm.lastLine() &&
                    anchor.line == head.line - 1
                ) {
                    // Special case for dd on last line (and first line).
                    if (anchor.line == cm.firstLine()) {
                        anchor.ch = 0
                    } else {
                        anchor = new Pos(
                            anchor.line - 1,
                            lineLength(cm, anchor.line - 1)
                        )
                    }
                }
                text = cm.getRange(anchor, head)
                cm.replaceRange('', anchor, head)
                finalHead = anchor
                if (args.linewise) {
                    finalHead = motions.moveToFirstNonWhiteSpaceCharacter(
                        cm,
                        anchor
                    )
                }
            } else {
                text = cm.getSelection()
                var replacement = fillArray('', ranges.length)
                cm.replaceSelections(replacement)
                finalHead = cursorMin(ranges[0].head, ranges[0].anchor)
            }
            vimGlobalState.registerController.pushText(
                args.registerName,
                'delete',
                text,
                args.linewise,
                vim.visualBlock
            )
            return clipCursorToContent(cm, finalHead)
        },
        indent: function (cm, args, ranges) {
            var vim = cm.state.vim
            if (cm.indentMore) {
                var repeat = vim.visualMode ? args.repeat : 1
                for (var j = 0; j < repeat; j++) {
                    if (args.indentRight) cm.indentMore()
                    else cm.indentLess()
                }
            } else {
                var startLine = ranges[0].anchor.line
                var endLine = vim.visualBlock
                    ? ranges[ranges.length - 1].anchor.line
                    : ranges[0].head.line
                // In visual mode, n> shifts the selection right n times, instead of
                // shifting n lines right once.
                var repeat = vim.visualMode ? args.repeat : 1
                if (args.linewise) {
                    // The only way to delete a newline is to delete until the start of
                    // the next line, so in linewise mode evalInput will include the next
                    // line. We don't want this in indent, so we go back a line.
                    endLine--
                }
                for (var i = startLine; i <= endLine; i++) {
                    for (var j = 0; j < repeat; j++) {
                        cm.indentLine(i, args.indentRight)
                    }
                }
            }
            return motions.moveToFirstNonWhiteSpaceCharacter(
                cm,
                ranges[0].anchor
            )
        },
        indentAuto: function (cm, _args, ranges) {
            cm.execCommand('indentAuto')
            return motions.moveToFirstNonWhiteSpaceCharacter(
                cm,
                ranges[0].anchor
            )
        },
        changeCase: function (cm, args, ranges, oldAnchor, newHead) {
            var selections = cm.getSelections()
            var swapped = []
            var toLower = args.toLower
            for (var j = 0; j < selections.length; j++) {
                var toSwap = selections[j]
                var text = ''
                if (toLower === true) {
                    text = toSwap.toLowerCase()
                } else if (toLower === false) {
                    text = toSwap.toUpperCase()
                } else {
                    for (var i = 0; i < toSwap.length; i++) {
                        var character = toSwap.charAt(i)
                        text += isUpperCase(character)
                            ? character.toLowerCase()
                            : character.toUpperCase()
                    }
                }
                swapped.push(text)
            }
            cm.replaceSelections(swapped)
            if (args.shouldMoveCursor) {
                return newHead
            } else if (
                !cm.state.vim.visualMode &&
                args.linewise &&
                ranges[0].anchor.line + 1 == ranges[0].head.line
            ) {
                return motions.moveToFirstNonWhiteSpaceCharacter(cm, oldAnchor)
            } else if (args.linewise) {
                return oldAnchor
            } else {
                return cursorMin(ranges[0].anchor, ranges[0].head)
            }
        },
        yank: function (cm, args, ranges, oldAnchor) {
            var vim = cm.state.vim
            var text = cm.getSelection()
            var endPos = vim.visualMode
                ? cursorMin(
                      vim.sel.anchor,
                      vim.sel.head,
                      ranges[0].head,
                      ranges[0].anchor
                  )
                : oldAnchor
            vimGlobalState.registerController.pushText(
                args.registerName,
                'yank',
                text,
                args.linewise,
                vim.visualBlock
            )
            return endPos
        },
    }

    function defineOperator(name, fn) {
        operators[name] = fn
    }

    var actions = {
        jumpListWalk: function (cm, actionArgs, vim) {
            if (vim.visualMode) {
                return
            }
            var repeat = actionArgs.repeat
            var forward = actionArgs.forward
            var jumpList = vimGlobalState.jumpList

            var mark = jumpList.move(cm, forward ? repeat : -repeat)
            var markPos = mark ? mark.find() : undefined
            markPos = markPos ? markPos : cm.getCursor()
            cm.setCursor(markPos)
        },
        scroll: function (cm, actionArgs, vim) {
            if (vim.visualMode) {
                return
            }
            var repeat = actionArgs.repeat || 1
            var lineHeight = cm.defaultTextHeight()
            var top = cm.getScrollInfo().top
            var delta = lineHeight * repeat
            var newPos = actionArgs.forward ? top + delta : top - delta
            var cursor = copyCursor(cm.getCursor())
            var cursorCoords = cm.charCoords(cursor, 'local')
            if (actionArgs.forward) {
                if (newPos > cursorCoords.top) {
                    cursor.line += (newPos - cursorCoords.top) / lineHeight
                    cursor.line = Math.ceil(cursor.line)
                    cm.setCursor(cursor)
                    cursorCoords = cm.charCoords(cursor, 'local')
                    cm.scrollTo(null, cursorCoords.top)
                } else {
                    // Cursor stays within bounds.  Just reposition the scroll window.
                    cm.scrollTo(null, newPos)
                }
            } else {
                var newBottom = newPos + cm.getScrollInfo().clientHeight
                if (newBottom < cursorCoords.bottom) {
                    cursor.line -=
                        (cursorCoords.bottom - newBottom) / lineHeight
                    cursor.line = Math.floor(cursor.line)
                    cm.setCursor(cursor)
                    cursorCoords = cm.charCoords(cursor, 'local')
                    cm.scrollTo(
                        null,
                        cursorCoords.bottom - cm.getScrollInfo().clientHeight
                    )
                } else {
                    // Cursor stays within bounds.  Just reposition the scroll window.
                    cm.scrollTo(null, newPos)
                }
            }
        },
        scrollToCursor: function (cm, actionArgs) {
            var lineNum = cm.getCursor().line
            var charCoords = cm.charCoords(new Pos(lineNum, 0), 'local')
            var height = cm.getScrollInfo().clientHeight
            var y = charCoords.top
            switch (actionArgs.position) {
                case 'center':
                    y = charCoords.bottom - height / 2
                    break
                case 'bottom':
                    var lineLastCharPos = new Pos(
                        lineNum,
                        cm.getLine(lineNum).length - 1
                    )
                    var lineLastCharCoords = cm.charCoords(
                        lineLastCharPos,
                        'local'
                    )
                    var lineHeight = lineLastCharCoords.bottom - y
                    y = y - height + lineHeight
                    break
            }
            cm.scrollTo(null, y)
        },
        replayMacro: function (cm, actionArgs, vim) {
            var registerName = actionArgs.selectedCharacter
            var repeat = actionArgs.repeat
            var macroModeState = vimGlobalState.macroModeState
            if (registerName == '@') {
                registerName = macroModeState.latestRegister
            } else {
                macroModeState.latestRegister = registerName
            }
            while (repeat--) {
                executeMacroRegister(cm, vim, macroModeState, registerName)
            }
        },
        enterMacroRecordMode: function (cm, actionArgs) {
            var macroModeState = vimGlobalState.macroModeState
            var registerName = actionArgs.selectedCharacter
            if (
                vimGlobalState.registerController.isValidRegister(registerName)
            ) {
                macroModeState.enterMacroRecordMode(cm, registerName)
            }
        },
        toggleOverwrite: function (cm) {
            if (!cm.state.overwrite) {
                cm.toggleOverwrite(true)
                cm.setOption('keyMap', 'vim-replace')
                CodeMirror.signal(cm, 'vim-mode-change', { mode: 'replace' })
            } else {
                cm.toggleOverwrite(false)
                cm.setOption('keyMap', 'vim-insert')
                CodeMirror.signal(cm, 'vim-mode-change', { mode: 'insert' })
            }
        },
        enterInsertMode: function (cm, actionArgs, vim) {
            if (cm.getOption('readOnly')) {
                return
            }
            vim.insertMode = true
            vim.insertModeRepeat = (actionArgs && actionArgs.repeat) || 1
            var insertAt = actionArgs ? actionArgs.insertAt : null
            var sel = vim.sel
            var head = actionArgs.head || cm.getCursor('head')
            var height = cm.listSelections().length
            if (insertAt == 'eol') {
                head = new Pos(head.line, lineLength(cm, head.line))
            } else if (insertAt == 'bol') {
                head = new Pos(head.line, 0)
            } else if (insertAt == 'charAfter') {
                var newPosition = updateSelectionForSurrogateCharacters(
                    cm,
                    head,
                    offsetCursor(head, 0, 1)
                )
                head = newPosition.end
            } else if (insertAt == 'firstNonBlank') {
                var newPosition = updateSelectionForSurrogateCharacters(
                    cm,
                    head,
                    motions.moveToFirstNonWhiteSpaceCharacter(cm, head)
                )
                head = newPosition.end
            } else if (insertAt == 'startOfSelectedArea') {
                if (!vim.visualMode) return
                if (!vim.visualBlock) {
                    if (sel.head.line < sel.anchor.line) {
                        head = sel.head
                    } else {
                        head = new Pos(sel.anchor.line, 0)
                    }
                } else {
                    head = new Pos(
                        Math.min(sel.head.line, sel.anchor.line),
                        Math.min(sel.head.ch, sel.anchor.ch)
                    )
                    height = Math.abs(sel.head.line - sel.anchor.line) + 1
                }
            } else if (insertAt == 'endOfSelectedArea') {
                if (!vim.visualMode) return
                if (!vim.visualBlock) {
                    if (sel.head.line >= sel.anchor.line) {
                        head = offsetCursor(sel.head, 0, 1)
                    } else {
                        head = new Pos(sel.anchor.line, 0)
                    }
                } else {
                    head = new Pos(
                        Math.min(sel.head.line, sel.anchor.line),
                        Math.max(sel.head.ch, sel.anchor.ch) + 1
                    )
                    height = Math.abs(sel.head.line - sel.anchor.line) + 1
                }
            } else if (insertAt == 'inplace') {
                if (vim.visualMode) {
                    return
                }
            } else if (insertAt == 'lastEdit') {
                head = getLastEditPos(cm) || head
            }
            cm.setOption('disableInput', false)
            if (actionArgs && actionArgs.replace) {
                // Handle Replace-mode as a special case of insert mode.
                cm.toggleOverwrite(true)
                cm.setOption('keyMap', 'vim-replace')
                CodeMirror.signal(cm, 'vim-mode-change', { mode: 'replace' })
            } else {
                cm.toggleOverwrite(false)
                cm.setOption('keyMap', 'vim-insert')
                CodeMirror.signal(cm, 'vim-mode-change', { mode: 'insert' })
            }
            if (!vimGlobalState.macroModeState.isPlaying) {
                // Only record if not replaying.
                cm.on('change', onChange)
                CodeMirror.on(
                    cm.getInputField(),
                    'keydown',
                    onKeyEventTargetKeyDown
                )
            }
            if (vim.visualMode) {
                exitVisualMode(cm)
            }
            selectForInsert(cm, head, height)
        },
        toggleVisualMode: function (cm, actionArgs, vim) {
            var repeat = actionArgs.repeat
            var anchor = cm.getCursor()
            var head
            // TODO: The repeat should actually select number of characters/lines
            //     equal to the repeat times the size of the previous visual
            //     operation.
            if (!vim.visualMode) {
                // Entering visual mode
                vim.visualMode = true
                vim.visualLine = !!actionArgs.linewise
                vim.visualBlock = !!actionArgs.blockwise
                head = clipCursorToContent(
                    cm,
                    new Pos(anchor.line, anchor.ch + repeat - 1)
                )
                var newPosition = updateSelectionForSurrogateCharacters(
                    cm,
                    anchor,
                    head
                )
                vim.sel = {
                    anchor: newPosition.start,
                    head: newPosition.end,
                }
                CodeMirror.signal(cm, 'vim-mode-change', {
                    mode: 'visual',
                    subMode: vim.visualLine
                        ? 'linewise'
                        : vim.visualBlock
                        ? 'blockwise'
                        : '',
                })
                updateCmSelection(cm)
                updateMark(cm, vim, '<', cursorMin(anchor, head))
                updateMark(cm, vim, '>', cursorMax(anchor, head))
            } else if (
                vim.visualLine ^ actionArgs.linewise ||
                vim.visualBlock ^ actionArgs.blockwise
            ) {
                // Toggling between modes
                vim.visualLine = !!actionArgs.linewise
                vim.visualBlock = !!actionArgs.blockwise
                CodeMirror.signal(cm, 'vim-mode-change', {
                    mode: 'visual',
                    subMode: vim.visualLine
                        ? 'linewise'
                        : vim.visualBlock
                        ? 'blockwise'
                        : '',
                })
                updateCmSelection(cm)
            } else {
                exitVisualMode(cm)
            }
        },
        reselectLastSelection: function (cm, _actionArgs, vim) {
            var lastSelection = vim.lastSelection
            if (vim.visualMode) {
                updateLastSelection(cm, vim)
            }
            if (lastSelection) {
                var anchor = lastSelection.anchorMark.find()
                var head = lastSelection.headMark.find()
                if (!anchor || !head) {
                    // If the marks have been destroyed due to edits, do nothing.
                    return
                }
                vim.sel = {
                    anchor: anchor,
                    head: head,
                }
                vim.visualMode = true
                vim.visualLine = lastSelection.visualLine
                vim.visualBlock = lastSelection.visualBlock
                updateCmSelection(cm)
                updateMark(cm, vim, '<', cursorMin(anchor, head))
                updateMark(cm, vim, '>', cursorMax(anchor, head))
                CodeMirror.signal(cm, 'vim-mode-change', {
                    mode: 'visual',
                    subMode: vim.visualLine
                        ? 'linewise'
                        : vim.visualBlock
                        ? 'blockwise'
                        : '',
                })
            }
        },
        joinLines: function (cm, actionArgs, vim) {
            var curStart, curEnd
            if (vim.visualMode) {
                curStart = cm.getCursor('anchor')
                curEnd = cm.getCursor('head')
                if (cursorIsBefore(curEnd, curStart)) {
                    var tmp = curEnd
                    curEnd = curStart
                    curStart = tmp
                }
                curEnd.ch = lineLength(cm, curEnd.line) - 1
            } else {
                // Repeat is the number of lines to join. Minimum 2 lines.
                var repeat = Math.max(actionArgs.repeat, 2)
                curStart = cm.getCursor()
                curEnd = clipCursorToContent(
                    cm,
                    new Pos(curStart.line + repeat - 1, Infinity)
                )
            }
            var finalCh = 0
            for (var i = curStart.line; i < curEnd.line; i++) {
                finalCh = lineLength(cm, curStart.line)
                var text = ''
                var nextStartCh = 0
                if (!actionArgs.keepSpaces) {
                    var nextLine = cm.getLine(curStart.line + 1)
                    nextStartCh = nextLine.search(/\S/)
                    if (nextStartCh == -1) {
                        nextStartCh = nextLine.length
                    } else {
                        text = ' '
                    }
                }
                cm.replaceRange(
                    text,
                    new Pos(curStart.line, finalCh),
                    new Pos(curStart.line + 1, nextStartCh)
                )
            }
            var curFinalPos = clipCursorToContent(
                cm,
                new Pos(curStart.line, finalCh)
            )
            if (vim.visualMode) {
                exitVisualMode(cm, false)
            }
            cm.setCursor(curFinalPos)
        },
        newLineAndEnterInsertMode: function (cm, actionArgs, vim) {
            vim.insertMode = true
            var insertAt = copyCursor(cm.getCursor())
            if (insertAt.line === cm.firstLine() && !actionArgs.after) {
                // Special case for inserting newline before start of document.
                cm.replaceRange('\n', new Pos(cm.firstLine(), 0))
                cm.setCursor(cm.firstLine(), 0)
            } else {
                insertAt.line = actionArgs.after
                    ? insertAt.line
                    : insertAt.line - 1
                insertAt.ch = lineLength(cm, insertAt.line)
                cm.setCursor(insertAt)
                var newlineFn =
                    CodeMirror.commands.newlineAndIndentContinueComment ||
                    CodeMirror.commands.newlineAndIndent
                newlineFn(cm)
            }
            this.enterInsertMode(cm, { repeat: actionArgs.repeat }, vim)
        },
        paste: function (cm, actionArgs, vim) {
            var register = vimGlobalState.registerController.getRegister(
                actionArgs.registerName
            )
            if (actionArgs.registerName === '+') {
                navigator.clipboard.readText().then((value) => {
                    this.continuePaste(cm, actionArgs, vim, value, register)
                })
            } else {
                var text = register.toString()
                this.continuePaste(cm, actionArgs, vim, text, register)
            }
        },
        continuePaste: function (cm, actionArgs, vim, text, register) {
            var cur = copyCursor(cm.getCursor())
            if (!text) {
                return
            }
            if (actionArgs.matchIndent) {
                var tabSize = cm.getOption('tabSize')
                // length that considers tabs and tabSize
                var whitespaceLength = function (str) {
                    var tabs = str.split('\t').length - 1
                    var spaces = str.split(' ').length - 1
                    return tabs * tabSize + spaces * 1
                }
                var currentLine = cm.getLine(cm.getCursor().line)
                var indent = whitespaceLength(currentLine.match(/^\s*/)[0])
                // chomp last newline b/c don't want it to match /^\s*/gm
                var chompedText = text.replace(/\n$/, '')
                var wasChomped = text !== chompedText
                var firstIndent = whitespaceLength(text.match(/^\s*/)[0])
                var text = chompedText.replace(/^\s*/gm, function (wspace) {
                    var newIndent =
                        indent + (whitespaceLength(wspace) - firstIndent)
                    if (newIndent < 0) {
                        return ''
                    } else if (cm.getOption('indentWithTabs')) {
                        var quotient = Math.floor(newIndent / tabSize)
                        return Array(quotient + 1).join('\t')
                    } else {
                        return Array(newIndent + 1).join(' ')
                    }
                })
                text += wasChomped ? '\n' : ''
            }
            if (actionArgs.repeat > 1) {
                var text = Array(actionArgs.repeat + 1).join(text)
            }
            var linewise = register.linewise
            var blockwise = register.blockwise
            if (blockwise) {
                text = text.split('\n')
                if (linewise) {
                    text.pop()
                }
                for (var i = 0; i < text.length; i++) {
                    text[i] = text[i] == '' ? ' ' : text[i]
                }
                cur.ch += actionArgs.after ? 1 : 0
                cur.ch = Math.min(lineLength(cm, cur.line), cur.ch)
            } else if (linewise) {
                if (vim.visualMode) {
                    text = vim.visualLine
                        ? text.slice(0, -1)
                        : '\n' + text.slice(0, text.length - 1) + '\n'
                } else if (actionArgs.after) {
                    // Move the newline at the end to the start instead, and paste just
                    // before the newline character of the line we are on right now.
                    text = '\n' + text.slice(0, text.length - 1)
                    cur.ch = lineLength(cm, cur.line)
                } else {
                    cur.ch = 0
                }
            } else {
                cur.ch += actionArgs.after ? 1 : 0
            }
            var curPosFinal
            var idx
            if (vim.visualMode) {
                //  save the pasted text for reselection if the need arises
                vim.lastPastedText = text
                var lastSelectionCurEnd
                var selectedArea = getSelectedAreaRange(cm, vim)
                var selectionStart = selectedArea[0]
                var selectionEnd = selectedArea[1]
                var selectedText = cm.getSelection()
                var selections = cm.listSelections()
                var emptyStrings = new Array(selections.length)
                    .join('1')
                    .split('1')
                // save the curEnd marker before it get cleared due to cm.replaceRange.
                if (vim.lastSelection) {
                    lastSelectionCurEnd = vim.lastSelection.headMark.find()
                }
                // push the previously selected text to unnamed register
                vimGlobalState.registerController.unnamedRegister.setText(
                    selectedText
                )
                if (blockwise) {
                    // first delete the selected text
                    cm.replaceSelections(emptyStrings)
                    // Set new selections as per the block length of the yanked text
                    selectionEnd = new Pos(
                        selectionStart.line + text.length - 1,
                        selectionStart.ch
                    )
                    cm.setCursor(selectionStart)
                    selectBlock(cm, selectionEnd)
                    cm.replaceSelections(text)
                    curPosFinal = selectionStart
                } else if (vim.visualBlock) {
                    cm.replaceSelections(emptyStrings)
                    cm.setCursor(selectionStart)
                    cm.replaceRange(text, selectionStart, selectionStart)
                    curPosFinal = selectionStart
                } else {
                    cm.replaceRange(text, selectionStart, selectionEnd)
                    curPosFinal = cm.posFromIndex(
                        cm.indexFromPos(selectionStart) + text.length - 1
                    )
                }
                // restore the the curEnd marker
                if (lastSelectionCurEnd) {
                    vim.lastSelection.headMark =
                        cm.setBookmark(lastSelectionCurEnd)
                }
                if (linewise) {
                    curPosFinal.ch = 0
                }
            } else {
                if (blockwise) {
                    cm.setCursor(cur)
                    for (var i = 0; i < text.length; i++) {
                        var line = cur.line + i
                        if (line > cm.lastLine()) {
                            cm.replaceRange('\n', new Pos(line, 0))
                        }
                        var lastCh = lineLength(cm, line)
                        if (lastCh < cur.ch) {
                            extendLineToColumn(cm, line, cur.ch)
                        }
                    }
                    cm.setCursor(cur)
                    selectBlock(cm, new Pos(cur.line + text.length - 1, cur.ch))
                    cm.replaceSelections(text)
                    curPosFinal = cur
                } else {
                    cm.replaceRange(text, cur)
                    // Now fine tune the cursor to where we want it.
                    if (linewise && actionArgs.after) {
                        curPosFinal = new Pos(
                            cur.line + 1,
                            findFirstNonWhiteSpaceCharacter(
                                cm.getLine(cur.line + 1)
                            )
                        )
                    } else if (linewise && !actionArgs.after) {
                        curPosFinal = new Pos(
                            cur.line,
                            findFirstNonWhiteSpaceCharacter(
                                cm.getLine(cur.line)
                            )
                        )
                    } else if (!linewise && actionArgs.after) {
                        idx = cm.indexFromPos(cur)
                        curPosFinal = cm.posFromIndex(idx + text.length - 1)
                    } else {
                        idx = cm.indexFromPos(cur)
                        curPosFinal = cm.posFromIndex(idx + text.length)
                    }
                }
            }
            if (vim.visualMode) {
                exitVisualMode(cm, false)
            }
            cm.setCursor(curPosFinal)
        },
        undo: function (cm, actionArgs) {
            cm.operation(function () {
                repeatFn(cm, CodeMirror.commands.undo, actionArgs.repeat)()
                cm.setCursor(clipCursorToContent(cm, cm.getCursor('start')))
            })
        },
        redo: function (cm, actionArgs) {
            repeatFn(cm, CodeMirror.commands.redo, actionArgs.repeat)()
        },
        setRegister: function (_cm, actionArgs, vim) {
            vim.inputState.registerName = actionArgs.selectedCharacter
        },
        insertRegister: function (cm, actionArgs, vim) {
            var registerName = actionArgs.selectedCharacter
            var register =
                vimGlobalState.registerController.getRegister(registerName)
            var text = register && register.toString()
            if (text) {
                cm.replaceSelection(text)
            }
        },
        setMark: function (cm, actionArgs, vim) {
            var markName = actionArgs.selectedCharacter
            updateMark(cm, vim, markName, cm.getCursor())
        },
        replace: function (cm, actionArgs, vim) {
            var replaceWith = actionArgs.selectedCharacter
            var curStart = cm.getCursor()
            var replaceTo
            var curEnd
            var selections = cm.listSelections()
            if (vim.visualMode) {
                curStart = cm.getCursor('start')
                curEnd = cm.getCursor('end')
            } else {
                var line = cm.getLine(curStart.line)
                replaceTo = curStart.ch + actionArgs.repeat
                if (replaceTo > line.length) {
                    replaceTo = line.length
                }
                curEnd = new Pos(curStart.line, replaceTo)
            }

            var newPositions = updateSelectionForSurrogateCharacters(
                cm,
                curStart,
                curEnd
            )
            curStart = newPositions.start
            curEnd = newPositions.end
            if (replaceWith == '\n') {
                if (!vim.visualMode)
                    cm.replaceRange('', curStart, curEnd)
                    // special case, where vim help says to replace by just one line-break
                ;(
                    CodeMirror.commands.newlineAndIndentContinueComment ||
                    CodeMirror.commands.newlineAndIndent
                )(cm)
            } else {
                var replaceWithStr = cm.getRange(curStart, curEnd)
                // replace all surrogate characters with selected character
                replaceWithStr = replaceWithStr.replace(
                    /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
                    replaceWith
                )
                //replace all characters in range by selected, but keep linebreaks
                replaceWithStr = replaceWithStr.replace(/[^\n]/g, replaceWith)
                if (vim.visualBlock) {
                    // Tabs are split in visua block before replacing
                    var spaces = new Array(cm.getOption('tabSize') + 1).join(
                        ' '
                    )
                    replaceWithStr = cm.getSelection()
                    replaceWithStr = replaceWithStr.replace(
                        /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
                        replaceWith
                    )
                    replaceWithStr = replaceWithStr
                        .replace(/\t/g, spaces)
                        .replace(/[^\n]/g, replaceWith)
                        .split('\n')
                    cm.replaceSelections(replaceWithStr)
                } else {
                    cm.replaceRange(replaceWithStr, curStart, curEnd)
                }
                if (vim.visualMode) {
                    curStart = cursorIsBefore(
                        selections[0].anchor,
                        selections[0].head
                    )
                        ? selections[0].anchor
                        : selections[0].head
                    cm.setCursor(curStart)
                    exitVisualMode(cm, false)
                } else {
                    cm.setCursor(offsetCursor(curEnd, 0, -1))
                }
            }
        },
        incrementNumberToken: function (cm, actionArgs) {
            var cur = cm.getCursor()
            var lineStr = cm.getLine(cur.line)
            var re = /(-?)(?:(0x)([\da-f]+)|(0b|0|)(\d+))/gi
            var match
            var start
            var end
            var numberStr
            while ((match = re.exec(lineStr)) !== null) {
                start = match.index
                end = start + match[0].length
                if (cur.ch < end) break
            }
            if (!actionArgs.backtrack && end <= cur.ch) return
            if (match) {
                var baseStr = match[2] || match[4]
                var digits = match[3] || match[5]
                var increment = actionArgs.increase ? 1 : -1
                var base = { '0b': 2, 0: 8, '': 10, '0x': 16 }[
                    baseStr.toLowerCase()
                ]
                var number =
                    parseInt(match[1] + digits, base) +
                    increment * actionArgs.repeat
                numberStr = number.toString(base)
                var zeroPadding = baseStr
                    ? new Array(
                          digits.length - numberStr.length + 1 + match[1].length
                      ).join('0')
                    : ''
                if (numberStr.charAt(0) === '-') {
                    numberStr =
                        '-' + baseStr + zeroPadding + numberStr.substr(1)
                } else {
                    numberStr = baseStr + zeroPadding + numberStr
                }
                var from = new Pos(cur.line, start)
                var to = new Pos(cur.line, end)
                cm.replaceRange(numberStr, from, to)
            } else {
                return
            }
            cm.setCursor(new Pos(cur.line, start + numberStr.length - 1))
        },
        repeatLastEdit: function (cm, actionArgs, vim) {
            var lastEditInputState = vim.lastEditInputState
            if (!lastEditInputState) {
                return
            }
            var repeat = actionArgs.repeat
            if (repeat && actionArgs.repeatIsExplicit) {
                vim.lastEditInputState.repeatOverride = repeat
            } else {
                repeat = vim.lastEditInputState.repeatOverride || repeat
            }
            repeatLastEdit(cm, vim, repeat, false /** repeatForInsert */)
        },
        indent: function (cm, actionArgs) {
            cm.indentLine(cm.getCursor().line, actionArgs.indentRight)
        },
        exitInsertMode: exitInsertMode,
    }

    function defineAction(name, fn) {
        actions[name] = fn
    }

    /*
     * Below are miscellaneous utility functions used by vim.js
     */

    /**
     * Clips cursor to ensure that line is within the buffer's range
     * and is not inside surrogate pair
     * If includeLineBreak is true, then allow cur.ch == lineLength.
     */
    function clipCursorToContent(cm, cur, oldCur) {
        var vim = cm.state.vim
        var includeLineBreak = vim.insertMode || vim.visualMode
        var line = Math.min(Math.max(cm.firstLine(), cur.line), cm.lastLine())
        var text = cm.getLine(line)
        var maxCh = text.length - 1 + Number(!!includeLineBreak)
        var ch = Math.min(Math.max(0, cur.ch), maxCh)
        // prevent cursor from entering surrogate pair
        var charCode = text.charCodeAt(ch)
        if (0xdc00 <= charCode && charCode <= 0xdfff) {
            var direction = 1
            if (oldCur && oldCur.line == line && oldCur.ch > ch) {
                direction = -1
            }
            ch += direction
            if (ch > maxCh) ch -= 2
        }
        return new Pos(line, ch)
    }
    function copyArgs(args) {
        var ret = {}
        for (var prop in args) {
            if (args.hasOwnProperty(prop)) {
                ret[prop] = args[prop]
            }
        }
        return ret
    }
    function offsetCursor(cur, offsetLine, offsetCh) {
        if (typeof offsetLine === 'object') {
            offsetCh = offsetLine.ch
            offsetLine = offsetLine.line
        }
        return new Pos(cur.line + offsetLine, cur.ch + offsetCh)
    }
    function commandMatches(keys, keyMap, context, inputState) {
        // Partial matches are not applied. They inform the key handler
        // that the current key sequence is a subsequence of a valid key
        // sequence, so that the key buffer is not cleared.
        var match,
            partial = [],
            full = []
        for (var i = 0; i < keyMap.length; i++) {
            var command = keyMap[i]
            if (
                (context == 'insert' && command.context != 'insert') ||
                (command.context && command.context != context) ||
                (inputState.operator && command.type == 'action') ||
                !(match = commandMatch(keys, command.keys))
            ) {
                continue
            }
            if (match == 'partial') {
                partial.push(command)
            }
            if (match == 'full') {
                full.push(command)
            }
        }
        return {
            partial: partial.length && partial,
            full: full.length && full,
        }
    }
    function commandMatch(pressed, mapped) {
        if (mapped.slice(-11) == '<character>') {
            // Last character matches anything.
            var prefixLen = mapped.length - 11
            var pressedPrefix = pressed.slice(0, prefixLen)
            var mappedPrefix = mapped.slice(0, prefixLen)
            return pressedPrefix == mappedPrefix && pressed.length > prefixLen
                ? 'full'
                : mappedPrefix.indexOf(pressedPrefix) == 0
                ? 'partial'
                : false
        } else {
            return pressed == mapped
                ? 'full'
                : mapped.indexOf(pressed) == 0
                ? 'partial'
                : false
        }
    }
    function lastChar(keys) {
        var match = /^.*(<[^>]+>)$/.exec(keys)
        var selectedCharacter = match ? match[1] : keys.slice(-1)
        if (selectedCharacter.length > 1) {
            switch (selectedCharacter) {
                case '<CR>':
                    selectedCharacter = '\n'
                    break
                case '<Space>':
                    selectedCharacter = ' '
                    break
                default:
                    selectedCharacter = ''
                    break
            }
        }
        return selectedCharacter
    }
    function repeatFn(cm, fn, repeat) {
        return function () {
            for (var i = 0; i < repeat; i++) {
                fn(cm)
            }
        }
    }
    function copyCursor(cur) {
        return new Pos(cur.line, cur.ch)
    }
    function cursorEqual(cur1, cur2) {
        return cur1.ch == cur2.ch && cur1.line == cur2.line
    }
    function cursorIsBefore(cur1, cur2) {
        if (cur1.line < cur2.line) {
            return true
        }
        if (cur1.line == cur2.line && cur1.ch < cur2.ch) {
            return true
        }
        return false
    }
    function cursorMin(cur1, cur2) {
        if (arguments.length > 2) {
            cur2 = cursorMin.apply(
                undefined,
                Array.prototype.slice.call(arguments, 1)
            )
        }
        return cursorIsBefore(cur1, cur2) ? cur1 : cur2
    }
    function cursorMax(cur1, cur2) {
        if (arguments.length > 2) {
            cur2 = cursorMax.apply(
                undefined,
                Array.prototype.slice.call(arguments, 1)
            )
        }
        return cursorIsBefore(cur1, cur2) ? cur2 : cur1
    }
    function cursorIsBetween(cur1, cur2, cur3) {
        // returns true if cur2 is between cur1 and cur3.
        var cur1before2 = cursorIsBefore(cur1, cur2)
        var cur2before3 = cursorIsBefore(cur2, cur3)
        return cur1before2 && cur2before3
    }
    function lineLength(cm, lineNum) {
        return cm.getLine(lineNum).length
    }
    function trim(s) {
        if (s.trim) {
            return s.trim()
        }
        return s.replace(/^\s+|\s+$/g, '')
    }
    function escapeRegex(s) {
        return s.replace(/([.?*+$\[\]\/\\(){}|\-])/g, '\\$1')
    }
    function extendLineToColumn(cm, lineNum, column) {
        var endCh = lineLength(cm, lineNum)
        var spaces = new Array(column - endCh + 1).join(' ')
        cm.setCursor(new Pos(lineNum, endCh))
        cm.replaceRange(spaces, cm.getCursor())
    }
    // This functions selects a rectangular block
    // of text with selectionEnd as any of its corner
    // Height of block:
    // Difference in selectionEnd.line and first/last selection.line
    // Width of the block:
    // Distance between selectionEnd.ch and any(first considered here) selection.ch
    function selectBlock(cm, selectionEnd) {
        var selections = [],
            ranges = cm.listSelections()
        var head = copyCursor(cm.clipPos(selectionEnd))
        var isClipped = !cursorEqual(selectionEnd, head)
        var curHead = cm.getCursor('head')
        var primIndex = getIndex(ranges, curHead)
        var wasClipped = cursorEqual(
            ranges[primIndex].head,
            ranges[primIndex].anchor
        )
        var max = ranges.length - 1
        var index = max - primIndex > primIndex ? max : 0
        var base = ranges[index].anchor

        var firstLine = Math.min(base.line, head.line)
        var lastLine = Math.max(base.line, head.line)
        var baseCh = base.ch,
            headCh = head.ch

        var dir = ranges[index].head.ch - baseCh
        var newDir = headCh - baseCh
        if (dir > 0 && newDir <= 0) {
            baseCh++
            if (!isClipped) {
                headCh--
            }
        } else if (dir < 0 && newDir >= 0) {
            baseCh--
            if (!wasClipped) {
                headCh++
            }
        } else if (dir < 0 && newDir == -1) {
            baseCh--
            headCh++
        }
        for (var line = firstLine; line <= lastLine; line++) {
            var range = {
                anchor: new Pos(line, baseCh),
                head: new Pos(line, headCh),
            }
            selections.push(range)
        }
        cm.setSelections(selections)
        selectionEnd.ch = headCh
        base.ch = baseCh
        return base
    }
    function selectForInsert(cm, head, height) {
        var sel = []
        for (var i = 0; i < height; i++) {
            var lineHead = offsetCursor(head, i, 0)
            sel.push({ anchor: lineHead, head: lineHead })
        }
        cm.setSelections(sel, 0)
    }
    // getIndex returns the index of the cursor in the selections.
    function getIndex(ranges, cursor, end) {
        for (var i = 0; i < ranges.length; i++) {
            var atAnchor =
                end != 'head' && cursorEqual(ranges[i].anchor, cursor)
            var atHead = end != 'anchor' && cursorEqual(ranges[i].head, cursor)
            if (atAnchor || atHead) {
                return i
            }
        }
        return -1
    }
    function getSelectedAreaRange(cm, vim) {
        var lastSelection = vim.lastSelection
        var getCurrentSelectedAreaRange = function () {
            var selections = cm.listSelections()
            var start = selections[0]
            var end = selections[selections.length - 1]
            var selectionStart = cursorIsBefore(start.anchor, start.head)
                ? start.anchor
                : start.head
            var selectionEnd = cursorIsBefore(end.anchor, end.head)
                ? end.head
                : end.anchor
            return [selectionStart, selectionEnd]
        }
        var getLastSelectedAreaRange = function () {
            var selectionStart = cm.getCursor()
            var selectionEnd = cm.getCursor()
            var block = lastSelection.visualBlock
            if (block) {
                var width = block.width
                var height = block.height
                selectionEnd = new Pos(
                    selectionStart.line + height,
                    selectionStart.ch + width
                )
                var selections = []
                // selectBlock creates a 'proper' rectangular block.
                // We do not want that in all cases, so we manually set selections.
                for (var i = selectionStart.line; i < selectionEnd.line; i++) {
                    var anchor = new Pos(i, selectionStart.ch)
                    var head = new Pos(i, selectionEnd.ch)
                    var range = { anchor: anchor, head: head }
                    selections.push(range)
                }
                cm.setSelections(selections)
            } else {
                var start = lastSelection.anchorMark.find()
                var end = lastSelection.headMark.find()
                var line = end.line - start.line
                var ch = end.ch - start.ch
                selectionEnd = {
                    line: selectionEnd.line + line,
                    ch: line ? selectionEnd.ch : ch + selectionEnd.ch,
                }
                if (lastSelection.visualLine) {
                    selectionStart = new Pos(selectionStart.line, 0)
                    selectionEnd = new Pos(
                        selectionEnd.line,
                        lineLength(cm, selectionEnd.line)
                    )
                }
                cm.setSelection(selectionStart, selectionEnd)
            }
            return [selectionStart, selectionEnd]
        }
        if (!vim.visualMode) {
            // In case of replaying the action.
            return getLastSelectedAreaRange()
        } else {
            return getCurrentSelectedAreaRange()
        }
    }
    // Updates the previous selection with the current selection's values. This
    // should only be called in visual mode.
    function updateLastSelection(cm, vim) {
        var anchor = vim.sel.anchor
        var head = vim.sel.head
        // To accommodate the effect of lastPastedText in the last selection
        if (vim.lastPastedText) {
            head = cm.posFromIndex(
                cm.indexFromPos(anchor) + vim.lastPastedText.length
            )
            vim.lastPastedText = null
        }
        vim.lastSelection = {
            anchorMark: cm.setBookmark(anchor),
            headMark: cm.setBookmark(head),
            anchor: copyCursor(anchor),
            head: copyCursor(head),
            visualMode: vim.visualMode,
            visualLine: vim.visualLine,
            visualBlock: vim.visualBlock,
        }
    }
    function expandSelection(cm, start, end) {
        var sel = cm.state.vim.sel
        var head = sel.head
        var anchor = sel.anchor
        var tmp
        if (cursorIsBefore(end, start)) {
            tmp = end
            end = start
            start = tmp
        }
        if (cursorIsBefore(head, anchor)) {
            head = cursorMin(start, head)
            anchor = cursorMax(anchor, end)
        } else {
            anchor = cursorMin(start, anchor)
            head = cursorMax(head, end)
            head = offsetCursor(head, 0, -1)
            if (head.ch == -1 && head.line != cm.firstLine()) {
                head = new Pos(head.line - 1, lineLength(cm, head.line - 1))
            }
        }
        return [anchor, head]
    }
    /**
     * Updates the CodeMirror selection to match the provided vim selection.
     * If no arguments are given, it uses the current vim selection state.
     */
    function updateCmSelection(cm, sel, mode) {
        var vim = cm.state.vim
        sel = sel || vim.sel
        var mode =
            mode || vim.visualLine ? 'line' : vim.visualBlock ? 'block' : 'char'
        var cmSel = makeCmSelection(cm, sel, mode)
        cm.setSelections(cmSel.ranges, cmSel.primary)
    }
    function makeCmSelection(cm, sel, mode, exclusive) {
        var head = copyCursor(sel.head)
        var anchor = copyCursor(sel.anchor)
        if (mode == 'char') {
            var headOffset =
                !exclusive && !cursorIsBefore(sel.head, sel.anchor) ? 1 : 0
            var anchorOffset = cursorIsBefore(sel.head, sel.anchor) ? 1 : 0
            head = offsetCursor(sel.head, 0, headOffset)
            anchor = offsetCursor(sel.anchor, 0, anchorOffset)
            return {
                ranges: [{ anchor: anchor, head: head }],
                primary: 0,
            }
        } else if (mode == 'line') {
            if (!cursorIsBefore(sel.head, sel.anchor)) {
                anchor.ch = 0

                var lastLine = cm.lastLine()
                if (head.line > lastLine) {
                    head.line = lastLine
                }
                head.ch = lineLength(cm, head.line)
            } else {
                head.ch = 0
                anchor.ch = lineLength(cm, anchor.line)
            }
            return {
                ranges: [{ anchor: anchor, head: head }],
                primary: 0,
            }
        } else if (mode == 'block') {
            var top = Math.min(anchor.line, head.line),
                fromCh = anchor.ch,
                bottom = Math.max(anchor.line, head.line),
                toCh = head.ch
            if (fromCh < toCh) {
                toCh += 1
            } else {
                fromCh += 1
            }
            var height = bottom - top + 1
            var primary = head.line == top ? 0 : height - 1
            var ranges = []
            for (var i = 0; i < height; i++) {
                ranges.push({
                    anchor: new Pos(top + i, fromCh),
                    head: new Pos(top + i, toCh),
                })
            }
            return {
                ranges: ranges,
                primary: primary,
            }
        }
    }
    function getHead(cm) {
        var cur = cm.getCursor('head')
        if (cm.getSelection().length == 1) {
            // Small corner case when only 1 character is selected. The "real"
            // head is the left of head and anchor.
            cur = cursorMin(cur, cm.getCursor('anchor'))
        }
        return cur
    }

    /**
     * If moveHead is set to false, the CodeMirror selection will not be
     * touched. The caller assumes the responsibility of putting the cursor
     * in the right place.
     */
    function exitVisualMode(cm, moveHead) {
        var vim = cm.state.vim
        if (moveHead !== false) {
            cm.setCursor(clipCursorToContent(cm, vim.sel.head))
        }
        updateLastSelection(cm, vim)
        vim.visualMode = false
        vim.visualLine = false
        vim.visualBlock = false
        if (!vim.insertMode)
            CodeMirror.signal(cm, 'vim-mode-change', { mode: 'normal' })
    }

    // Remove any trailing newlines from the selection. For
    // example, with the caret at the start of the last word on the line,
    // 'dw' should word, but not the newline, while 'w' should advance the
    // caret to the first character of the next line.
    function clipToLine(cm, curStart, curEnd) {
        var selection = cm.getRange(curStart, curEnd)
        // Only clip if the selection ends with trailing newline + whitespace
        if (/\n\s*$/.test(selection)) {
            var lines = selection.split('\n')
            // We know this is all whitespace.
            lines.pop()

            // Cases:
            // 1. Last word is an empty line - do not clip the trailing '\n'
            // 2. Last word is not an empty line - clip the trailing '\n'
            var line
            // Find the line containing the last word, and clip all whitespace up
            // to it.
            for (
                var line = lines.pop();
                lines.length > 0 && line && isWhiteSpaceString(line);
                line = lines.pop()
            ) {
                curEnd.line--
                curEnd.ch = 0
            }
            // If the last word is not an empty line, clip an additional newline
            if (line) {
                curEnd.line--
                curEnd.ch = lineLength(cm, curEnd.line)
            } else {
                curEnd.ch = 0
            }
        }
    }

    // Expand the selection to line ends.
    function expandSelectionToLine(_cm, curStart, curEnd) {
        curStart.ch = 0
        curEnd.ch = 0
        curEnd.line++
    }

    function findFirstNonWhiteSpaceCharacter(text) {
        if (!text) {
            return 0
        }
        var firstNonWS = text.search(/\S/)
        return firstNonWS == -1 ? text.length : firstNonWS
    }

    function expandWordUnderCursor(cm, inclusive, _forward, bigWord, noSymbol) {
        var cur = getHead(cm)
        var line = cm.getLine(cur.line)
        var idx = cur.ch

        // Seek to first word or non-whitespace character, depending on if
        // noSymbol is true.
        var test = noSymbol ? wordCharTest[0] : bigWordCharTest[0]
        while (!test(line.charAt(idx))) {
            idx++
            if (idx >= line.length) {
                return null
            }
        }

        if (bigWord) {
            test = bigWordCharTest[0]
        } else {
            test = wordCharTest[0]
            if (!test(line.charAt(idx))) {
                test = wordCharTest[1]
            }
        }

        var end = idx,
            start = idx
        while (test(line.charAt(end)) && end < line.length) {
            end++
        }
        while (test(line.charAt(start)) && start >= 0) {
            start--
        }
        start++

        if (inclusive) {
            // If present, include all whitespace after word.
            // Otherwise, include all whitespace before word, except indentation.
            var wordEnd = end
            while (/\s/.test(line.charAt(end)) && end < line.length) {
                end++
            }
            if (wordEnd == end) {
                var wordStart = start
                while (/\s/.test(line.charAt(start - 1)) && start > 0) {
                    start--
                }
                if (!start) {
                    start = wordStart
                }
            }
        }
        return { start: new Pos(cur.line, start), end: new Pos(cur.line, end) }
    }

    /**
     * Depends on the following:
     *
     * - editor mode should be htmlmixedmode / xml
     * - mode/xml/xml.js should be loaded
     * - addon/fold/xml-fold.js should be loaded
     *
     * If any of the above requirements are not true, this function noops.
     *
     * This is _NOT_ a 100% accurate implementation of vim tag text objects.
     * The following caveats apply (based off cursory testing, I'm sure there
     * are other discrepancies):
     *
     * - Does not work inside comments:
     *   ```
     *   <!-- <div>broken</div> -->
     *   ```
     * - Does not work when tags have different cases:
     *   ```
     *   <div>broken</DIV>
     *   ```
     * - Does not work when cursor is inside a broken tag:
     *   ```
     *   <div><brok><en></div>
     *   ```
     */
    function expandTagUnderCursor(cm, head, inclusive) {
        var cur = head
        if (!CodeMirror.findMatchingTag || !CodeMirror.findEnclosingTag) {
            return { start: cur, end: cur }
        }

        var tags =
            CodeMirror.findMatchingTag(cm, head) ||
            CodeMirror.findEnclosingTag(cm, head)
        if (!tags || !tags.open || !tags.close) {
            return { start: cur, end: cur }
        }

        if (inclusive) {
            return { start: tags.open.from, end: tags.close.to }
        }
        return { start: tags.open.to, end: tags.close.from }
    }

    function recordJumpPosition(cm, oldCur, newCur) {
        if (!cursorEqual(oldCur, newCur)) {
            vimGlobalState.jumpList.add(cm, oldCur, newCur)
        }
    }

    function recordLastCharacterSearch(increment, args) {
        vimGlobalState.lastCharacterSearch.increment = increment
        vimGlobalState.lastCharacterSearch.forward = args.forward
        vimGlobalState.lastCharacterSearch.selectedCharacter =
            args.selectedCharacter
    }

    var symbolToMode = {
        '(': 'bracket',
        ')': 'bracket',
        '{': 'bracket',
        '}': 'bracket',
        '[': 'section',
        ']': 'section',
        '*': 'comment',
        '/': 'comment',
        m: 'method',
        M: 'method',
        '#': 'preprocess',
    }
    var findSymbolModes = {
        bracket: {
            isComplete: function (state) {
                if (state.nextCh === state.symb) {
                    state.depth++
                    if (state.depth >= 1) return true
                } else if (state.nextCh === state.reverseSymb) {
                    state.depth--
                }
                return false
            },
        },
        section: {
            init: function (state) {
                state.curMoveThrough = true
                state.symb =
                    (state.forward ? ']' : '[') === state.symb ? '{' : '}'
            },
            isComplete: function (state) {
                return state.index === 0 && state.nextCh === state.symb
            },
        },
        comment: {
            isComplete: function (state) {
                var found = state.lastCh === '*' && state.nextCh === '/'
                state.lastCh = state.nextCh
                return found
            },
        },
        // TODO: The original Vim implementation only operates on level 1 and 2.
        // The current implementation doesn't check for code block level and
        // therefore it operates on any levels.
        method: {
            init: function (state) {
                state.symb = state.symb === 'm' ? '{' : '}'
                state.reverseSymb = state.symb === '{' ? '}' : '{'
            },
            isComplete: function (state) {
                if (state.nextCh === state.symb) return true
                return false
            },
        },
        preprocess: {
            init: function (state) {
                state.index = 0
            },
            isComplete: function (state) {
                if (state.nextCh === '#') {
                    var token = state.lineText.match(/^#(\w+)/)[1]
                    if (token === 'endif') {
                        if (state.forward && state.depth === 0) {
                            return true
                        }
                        state.depth++
                    } else if (token === 'if') {
                        if (!state.forward && state.depth === 0) {
                            return true
                        }
                        state.depth--
                    }
                    if (token === 'else' && state.depth === 0) return true
                }
                return false
            },
        },
    }
    function findSymbol(cm, repeat, forward, symb) {
        var cur = copyCursor(cm.getCursor())
        var increment = forward ? 1 : -1
        var endLine = forward ? cm.lineCount() : -1
        var curCh = cur.ch
        var line = cur.line
        var lineText = cm.getLine(line)
        var state = {
            lineText: lineText,
            nextCh: lineText.charAt(curCh),
            lastCh: null,
            index: curCh,
            symb: symb,
            reverseSymb: (forward
                ? { ')': '(', '}': '{' }
                : { '(': ')', '{': '}' })[symb],
            forward: forward,
            depth: 0,
            curMoveThrough: false,
        }
        var mode = symbolToMode[symb]
        if (!mode) return cur
        var init = findSymbolModes[mode].init
        var isComplete = findSymbolModes[mode].isComplete
        if (init) {
            init(state)
        }
        while (line !== endLine && repeat) {
            state.index += increment
            state.nextCh = state.lineText.charAt(state.index)
            if (!state.nextCh) {
                line += increment
                state.lineText = cm.getLine(line) || ''
                if (increment > 0) {
                    state.index = 0
                } else {
                    var lineLen = state.lineText.length
                    state.index = lineLen > 0 ? lineLen - 1 : 0
                }
                state.nextCh = state.lineText.charAt(state.index)
            }
            if (isComplete(state)) {
                cur.line = line
                cur.ch = state.index
                repeat--
            }
        }
        if (state.nextCh || state.curMoveThrough) {
            return new Pos(line, state.index)
        }
        return cur
    }

    /*
     * Returns the boundaries of the next word. If the cursor in the middle of
     * the word, then returns the boundaries of the current word, starting at
     * the cursor. If the cursor is at the start/end of a word, and we are going
     * forward/backward, respectively, find the boundaries of the next word.
     *
     * @param {CodeMirror} cm CodeMirror object.
     * @param {Cursor} cur The cursor position.
     * @param {boolean} forward True to search forward. False to search
     *     backward.
     * @param {boolean} bigWord True if punctuation count as part of the word.
     *     False if only [a-zA-Z0-9] characters count as part of the word.
     * @param {boolean} emptyLineIsWord True if empty lines should be treated
     *     as words.
     * @return {Object{from:number, to:number, line: number}} The boundaries of
     *     the word, or null if there are no more words.
     */
    function findWord(cm, cur, forward, bigWord, emptyLineIsWord) {
        var lineNum = cur.line
        var pos = cur.ch
        var line = cm.getLine(lineNum)
        var dir = forward ? 1 : -1
        var charTests = bigWord ? bigWordCharTest : wordCharTest

        if (emptyLineIsWord && line == '') {
            lineNum += dir
            line = cm.getLine(lineNum)
            if (!isLine(cm, lineNum)) {
                return null
            }
            pos = forward ? 0 : line.length
        }

        while (true) {
            if (emptyLineIsWord && line == '') {
                return { from: 0, to: 0, line: lineNum }
            }
            var stop = dir > 0 ? line.length : -1
            var wordStart = stop,
                wordEnd = stop
            // Find bounds of next word.
            while (pos != stop) {
                var foundWord = false
                for (var i = 0; i < charTests.length && !foundWord; ++i) {
                    if (charTests[i](line.charAt(pos))) {
                        wordStart = pos
                        // Advance to end of word.
                        while (pos != stop && charTests[i](line.charAt(pos))) {
                            pos += dir
                        }
                        wordEnd = pos
                        foundWord = wordStart != wordEnd
                        if (
                            wordStart == cur.ch &&
                            lineNum == cur.line &&
                            wordEnd == wordStart + dir
                        ) {
                            // We started at the end of a word. Find the next one.
                            continue
                        } else {
                            return {
                                from: Math.min(wordStart, wordEnd + 1),
                                to: Math.max(wordStart, wordEnd),
                                line: lineNum,
                            }
                        }
                    }
                }
                if (!foundWord) {
                    pos += dir
                }
            }
            // Advance to next/prev line.
            lineNum += dir
            if (!isLine(cm, lineNum)) {
                return null
            }
            line = cm.getLine(lineNum)
            pos = dir > 0 ? 0 : line.length
        }
    }

    /**
     * @param {CodeMirror} cm CodeMirror object.
     * @param {Pos} cur The position to start from.
     * @param {int} repeat Number of words to move past.
     * @param {boolean} forward True to search forward. False to search
     *     backward.
     * @param {boolean} wordEnd True to move to end of word. False to move to
     *     beginning of word.
     * @param {boolean} bigWord True if punctuation count as part of the word.
     *     False if only alphabet characters count as part of the word.
     * @return {Cursor} The position the cursor should move to.
     */
    function moveToWord(cm, cur, repeat, forward, wordEnd, bigWord) {
        var curStart = copyCursor(cur)
        var words = []
        if ((forward && !wordEnd) || (!forward && wordEnd)) {
            repeat++
        }
        // For 'e', empty lines are not considered words, go figure.
        var emptyLineIsWord = !(forward && wordEnd)
        for (var i = 0; i < repeat; i++) {
            var word = findWord(cm, cur, forward, bigWord, emptyLineIsWord)
            if (!word) {
                var eodCh = lineLength(cm, cm.lastLine())
                words.push(
                    forward
                        ? { line: cm.lastLine(), from: eodCh, to: eodCh }
                        : { line: 0, from: 0, to: 0 }
                )
                break
            }
            words.push(word)
            cur = new Pos(word.line, forward ? word.to - 1 : word.from)
        }
        var shortCircuit = words.length != repeat
        var firstWord = words[0]
        var lastWord = words.pop()
        if (forward && !wordEnd) {
            // w
            if (
                !shortCircuit &&
                (firstWord.from != curStart.ch ||
                    firstWord.line != curStart.line)
            ) {
                // We did not start in the middle of a word. Discard the extra word at the end.
                lastWord = words.pop()
            }
            return new Pos(lastWord.line, lastWord.from)
        } else if (forward && wordEnd) {
            return new Pos(lastWord.line, lastWord.to - 1)
        } else if (!forward && wordEnd) {
            // ge
            if (
                !shortCircuit &&
                (firstWord.to != curStart.ch || firstWord.line != curStart.line)
            ) {
                // We did not start in the middle of a word. Discard the extra word at the end.
                lastWord = words.pop()
            }
            return new Pos(lastWord.line, lastWord.to)
        } else {
            // b
            return new Pos(lastWord.line, lastWord.from)
        }
    }

    function moveToEol(cm, head, motionArgs, vim, keepHPos) {
        var cur = head
        var retval = new Pos(cur.line + motionArgs.repeat - 1, Infinity)
        var end = cm.clipPos(retval)
        end.ch--
        if (!keepHPos) {
            vim.lastHPos = Infinity
            vim.lastHSPos = cm.charCoords(end, 'div').left
        }
        return retval
    }

    function moveToCharacter(cm, repeat, forward, character) {
        var cur = cm.getCursor()
        var start = cur.ch
        var idx
        for (var i = 0; i < repeat; i++) {
            var line = cm.getLine(cur.line)
            idx = charIdxInLine(start, line, character, forward, true)
            if (idx == -1) {
                return null
            }
            start = idx
        }
        return new Pos(cm.getCursor().line, idx)
    }

    function moveToColumn(cm, repeat) {
        // repeat is always >= 1, so repeat - 1 always corresponds
        // to the column we want to go to.
        var line = cm.getCursor().line
        return clipCursorToContent(cm, new Pos(line, repeat - 1))
    }

    function updateMark(cm, vim, markName, pos) {
        if (!inArray(markName, validMarks)) {
            return
        }
        if (vim.marks[markName]) {
            vim.marks[markName].clear()
        }
        vim.marks[markName] = cm.setBookmark(pos)
    }

    function charIdxInLine(start, line, character, forward, includeChar) {
        // Search for char in line.
        // motion_options: {forward, includeChar}
        // If includeChar = true, include it too.
        // If forward = true, search forward, else search backwards.
        // If char is not found on this line, do nothing
        var idx
        if (forward) {
            idx = line.indexOf(character, start + 1)
            if (idx != -1 && !includeChar) {
                idx -= 1
            }
        } else {
            idx = line.lastIndexOf(character, start - 1)
            if (idx != -1 && !includeChar) {
                idx += 1
            }
        }
        return idx
    }

    function findParagraph(cm, head, repeat, dir, inclusive) {
        var line = head.line
        var min = cm.firstLine()
        var max = cm.lastLine()
        var start,
            end,
            i = line
        function isEmpty(i) {
            return !cm.getLine(i)
        }
        function isBoundary(i, dir, any) {
            if (any) {
                return isEmpty(i) != isEmpty(i + dir)
            }
            return !isEmpty(i) && isEmpty(i + dir)
        }
        if (dir) {
            while (min <= i && i <= max && repeat > 0) {
                if (isBoundary(i, dir)) {
                    repeat--
                }
                i += dir
            }
            return new Pos(i, 0)
        }

        var vim = cm.state.vim
        if (vim.visualLine && isBoundary(line, 1, true)) {
            var anchor = vim.sel.anchor
            if (isBoundary(anchor.line, -1, true)) {
                if (!inclusive || anchor.line != line) {
                    line += 1
                }
            }
        }
        var startState = isEmpty(line)
        for (i = line; i <= max && repeat; i++) {
            if (isBoundary(i, 1, true)) {
                if (!inclusive || isEmpty(i) != startState) {
                    repeat--
                }
            }
        }
        end = new Pos(i, 0)
        // select boundary before paragraph for the last one
        if (i > max && !startState) {
            startState = true
        } else {
            inclusive = false
        }
        for (i = line; i > min; i--) {
            if (!inclusive || isEmpty(i) == startState || i == line) {
                if (isBoundary(i, -1, true)) {
                    break
                }
            }
        }
        start = new Pos(i, 0)
        return { start: start, end: end }
    }

    /**
     * Based on {@link findSentence}. The internal functions have the same names,
     * but their behaviour is different. findSentence() crosses line breaks and
     * is used for jumping to sentence beginnings before or after the current cursor position,
     * whereas getSentence() is for getting the beginning or end of the sentence
     * at the current cursor position, either including (a) or excluding (i) whitespace.
     */
    function getSentence(
        cm,
        cur,
        repeat,
        dir,
        inclusive /*includes whitespace*/
    ) {
        /*
          Takes an index object
          {
            line: the line string,
            ln: line number,
            pos: index in line,
            dir: direction of traversal (-1 or 1)
          }
          and modifies the pos member to represent the
          next valid position or sets the line to null if there are
          no more valid positions.
         */
        function nextChar(curr) {
            if (
                curr.pos + curr.dir < 0 ||
                curr.pos + curr.dir >= curr.line.length
            ) {
                curr.line = null
            } else {
                curr.pos += curr.dir
            }
        }
        /*
          Performs one iteration of traversal in forward direction
          Returns an index object of the sentence end
         */
        function forward(cm, ln, pos, dir) {
            var line = cm.getLine(ln)

            var curr = {
                line: line,
                ln: ln,
                pos: pos,
                dir: dir,
            }

            if (curr.line === '') {
                return { ln: curr.ln, pos: curr.pos }
            }

            var lastSentencePos = curr.pos

            // Move one step to skip character we start on
            nextChar(curr)

            while (curr.line !== null) {
                lastSentencePos = curr.pos
                if (isEndOfSentenceSymbol(curr.line[curr.pos])) {
                    if (!inclusive) {
                        return { ln: curr.ln, pos: curr.pos + 1 }
                    } else {
                        nextChar(curr)
                        while (curr.line !== null) {
                            if (isWhiteSpaceString(curr.line[curr.pos])) {
                                lastSentencePos = curr.pos
                                nextChar(curr)
                            } else {
                                break
                            }
                        }
                        return { ln: curr.ln, pos: lastSentencePos + 1 }
                    }
                }
                nextChar(curr)
            }
            return { ln: curr.ln, pos: lastSentencePos + 1 }
        }

        /*
          Performs one iteration of traversal in reverse direction
          Returns an index object of the sentence start
         */
        function reverse(cm, ln, pos, dir) {
            var line = cm.getLine(ln)

            var curr = {
                line: line,
                ln: ln,
                pos: pos,
                dir: dir,
            }

            if (curr.line === '') {
                return { ln: curr.ln, pos: curr.pos }
            }

            var lastSentencePos = curr.pos

            // Move one step to skip character we start on
            nextChar(curr)

            while (curr.line !== null) {
                if (
                    !isWhiteSpaceString(curr.line[curr.pos]) &&
                    !isEndOfSentenceSymbol(curr.line[curr.pos])
                ) {
                    lastSentencePos = curr.pos
                } else if (isEndOfSentenceSymbol(curr.line[curr.pos])) {
                    if (!inclusive) {
                        return { ln: curr.ln, pos: lastSentencePos }
                    } else {
                        if (isWhiteSpaceString(curr.line[curr.pos + 1])) {
                            return { ln: curr.ln, pos: curr.pos + 1 }
                        } else {
                            return { ln: curr.ln, pos: lastSentencePos }
                        }
                    }
                }

                nextChar(curr)
            }
            curr.line = line
            if (inclusive && isWhiteSpaceString(curr.line[curr.pos])) {
                return { ln: curr.ln, pos: curr.pos }
            } else {
                return { ln: curr.ln, pos: lastSentencePos }
            }
        }

        var curr_index = {
            ln: cur.line,
            pos: cur.ch,
        }

        while (repeat > 0) {
            if (dir < 0) {
                curr_index = reverse(cm, curr_index.ln, curr_index.pos, dir)
            } else {
                curr_index = forward(cm, curr_index.ln, curr_index.pos, dir)
            }
            repeat--
        }

        return new Pos(curr_index.ln, curr_index.pos)
    }

    function findSentence(cm, cur, repeat, dir) {
        /*
           Takes an index object
           {
             line: the line string,
             ln: line number,
             pos: index in line,
             dir: direction of traversal (-1 or 1)
           }
           and modifies the line, ln, and pos members to represent the
           next valid position or sets them to null if there are
           no more valid positions.
         */
        function nextChar(cm, idx) {
            if (idx.pos + idx.dir < 0 || idx.pos + idx.dir >= idx.line.length) {
                idx.ln += idx.dir
                if (!isLine(cm, idx.ln)) {
                    idx.line = null
                    idx.ln = null
                    idx.pos = null
                    return
                }
                idx.line = cm.getLine(idx.ln)
                idx.pos = idx.dir > 0 ? 0 : idx.line.length - 1
            } else {
                idx.pos += idx.dir
            }
        }

        /*
          Performs one iteration of traversal in forward direction
          Returns an index object of the new location
         */
        function forward(cm, ln, pos, dir) {
            var line = cm.getLine(ln)
            var stop = line === ''

            var curr = {
                line: line,
                ln: ln,
                pos: pos,
                dir: dir,
            }

            var last_valid = {
                ln: curr.ln,
                pos: curr.pos,
            }

            var skip_empty_lines = curr.line === ''

            // Move one step to skip character we start on
            nextChar(cm, curr)

            while (curr.line !== null) {
                last_valid.ln = curr.ln
                last_valid.pos = curr.pos

                if (curr.line === '' && !skip_empty_lines) {
                    return { ln: curr.ln, pos: curr.pos }
                } else if (
                    stop &&
                    curr.line !== '' &&
                    !isWhiteSpaceString(curr.line[curr.pos])
                ) {
                    return { ln: curr.ln, pos: curr.pos }
                } else if (
                    isEndOfSentenceSymbol(curr.line[curr.pos]) &&
                    !stop &&
                    (curr.pos === curr.line.length - 1 ||
                        isWhiteSpaceString(curr.line[curr.pos + 1]))
                ) {
                    stop = true
                }

                nextChar(cm, curr)
            }

            /*
            Set the position to the last non whitespace character on the last
            valid line in the case that we reach the end of the document.
          */
            var line = cm.getLine(last_valid.ln)
            last_valid.pos = 0
            for (var i = line.length - 1; i >= 0; --i) {
                if (!isWhiteSpaceString(line[i])) {
                    last_valid.pos = i
                    break
                }
            }

            return last_valid
        }

        /*
          Performs one iteration of traversal in reverse direction
          Returns an index object of the new location
         */
        function reverse(cm, ln, pos, dir) {
            var line = cm.getLine(ln)

            var curr = {
                line: line,
                ln: ln,
                pos: pos,
                dir: dir,
            }

            var last_valid = {
                ln: curr.ln,
                pos: null,
            }

            var skip_empty_lines = curr.line === ''

            // Move one step to skip character we start on
            nextChar(cm, curr)

            while (curr.line !== null) {
                if (curr.line === '' && !skip_empty_lines) {
                    if (last_valid.pos !== null) {
                        return last_valid
                    } else {
                        return { ln: curr.ln, pos: curr.pos }
                    }
                } else if (
                    isEndOfSentenceSymbol(curr.line[curr.pos]) &&
                    last_valid.pos !== null &&
                    !(
                        curr.ln === last_valid.ln &&
                        curr.pos + 1 === last_valid.pos
                    )
                ) {
                    return last_valid
                } else if (
                    curr.line !== '' &&
                    !isWhiteSpaceString(curr.line[curr.pos])
                ) {
                    skip_empty_lines = false
                    last_valid = { ln: curr.ln, pos: curr.pos }
                }

                nextChar(cm, curr)
            }

            /*
            Set the position to the first non whitespace character on the last
            valid line in the case that we reach the beginning of the document.
          */
            var line = cm.getLine(last_valid.ln)
            last_valid.pos = 0
            for (var i = 0; i < line.length; ++i) {
                if (!isWhiteSpaceString(line[i])) {
                    last_valid.pos = i
                    break
                }
            }
            return last_valid
        }

        var curr_index = {
            ln: cur.line,
            pos: cur.ch,
        }

        while (repeat > 0) {
            if (dir < 0) {
                curr_index = reverse(cm, curr_index.ln, curr_index.pos, dir)
            } else {
                curr_index = forward(cm, curr_index.ln, curr_index.pos, dir)
            }
            repeat--
        }

        return new Pos(curr_index.ln, curr_index.pos)
    }

    // TODO: perhaps this finagling of start and end positions belongs
    // in codemirror/replaceRange?
    function selectCompanionObject(cm, head, symb, inclusive) {
        var cur = head,
            start,
            end

        var bracketRegexp = {
            '(': /[()]/,
            ')': /[()]/,
            '[': /[[\]]/,
            ']': /[[\]]/,
            '{': /[{}]/,
            '}': /[{}]/,
            '<': /[<>]/,
            '>': /[<>]/,
        }[symb]
        var openSym = {
            '(': '(',
            ')': '(',
            '[': '[',
            ']': '[',
            '{': '{',
            '}': '{',
            '<': '<',
            '>': '<',
        }[symb]
        var curChar = cm.getLine(cur.line).charAt(cur.ch)
        // Due to the behavior of scanForBracket, we need to add an offset if the
        // cursor is on a matching open bracket.
        var offset = curChar === openSym ? 1 : 0

        start = cm.scanForBracket(
            new Pos(cur.line, cur.ch + offset),
            -1,
            undefined,
            { bracketRegex: bracketRegexp }
        )
        end = cm.scanForBracket(
            new Pos(cur.line, cur.ch + offset),
            1,
            undefined,
            { bracketRegex: bracketRegexp }
        )

        if (!start || !end) {
            return { start: cur, end: cur }
        }

        start = start.pos
        end = end.pos

        if (
            (start.line == end.line && start.ch > end.ch) ||
            start.line > end.line
        ) {
            var tmp = start
            start = end
            end = tmp
        }

        if (inclusive) {
            end.ch += 1
        } else {
            start.ch += 1
        }

        return { start: start, end: end }
    }

    // Takes in a symbol and a cursor and tries to simulate text objects that
    // have identical opening and closing symbols
    // TODO support across multiple lines
    function findBeginningAndEnd(cm, head, symb, inclusive) {
        var cur = copyCursor(head)
        var line = cm.getLine(cur.line)
        var chars = line.split('')
        var start, end, i, len
        var firstIndex = chars.indexOf(symb)

        // the decision tree is to always look backwards for the beginning first,
        // but if the cursor is in front of the first instance of the symb,
        // then move the cursor forward
        if (cur.ch < firstIndex) {
            cur.ch = firstIndex
            // Why is this line even here???
            // cm.setCursor(cur.line, firstIndex+1);
        }
        // otherwise if the cursor is currently on the closing symbol
        else if (firstIndex < cur.ch && chars[cur.ch] == symb) {
            end = cur.ch // assign end to the current cursor
            --cur.ch // make sure to look backwards
        }

        // if we're currently on the symbol, we've got a start
        if (chars[cur.ch] == symb && !end) {
            start = cur.ch + 1 // assign start to ahead of the cursor
        } else {
            // go backwards to find the start
            for (i = cur.ch; i > -1 && !start; i--) {
                if (chars[i] == symb) {
                    start = i + 1
                }
            }
        }

        // look forwards for the end symbol
        if (start && !end) {
            for (i = start, len = chars.length; i < len && !end; i++) {
                if (chars[i] == symb) {
                    end = i
                }
            }
        }

        // nothing found
        if (!start || !end) {
            return { start: cur, end: cur }
        }

        // include the symbols
        if (inclusive) {
            --start
            ++end
        }

        return {
            start: new Pos(cur.line, start),
            end: new Pos(cur.line, end),
        }
    }

    // Search functions
    defineOption('pcre', true, 'boolean')
    function SearchState() {}
    SearchState.prototype = {
        getQuery: function () {
            return vimGlobalState.query
        },
        setQuery: function (query) {
            vimGlobalState.query = query
        },
        getOverlay: function () {
            return this.searchOverlay
        },
        setOverlay: function (overlay) {
            this.searchOverlay = overlay
        },
        isReversed: function () {
            return vimGlobalState.isReversed
        },
        setReversed: function (reversed) {
            vimGlobalState.isReversed = reversed
        },
        getScrollbarAnnotate: function () {
            return this.annotate
        },
        setScrollbarAnnotate: function (annotate) {
            this.annotate = annotate
        },
    }
    function getSearchState(cm) {
        var vim = cm.state.vim
        return vim.searchState_ || (vim.searchState_ = new SearchState())
    }
    function splitBySlash(argString) {
        return splitBySeparator(argString, '/')
    }

    function findUnescapedSlashes(argString) {
        return findUnescapedSeparators(argString, '/')
    }

    function splitBySeparator(argString, separator) {
        var slashes = findUnescapedSeparators(argString, separator) || []
        if (!slashes.length) return []
        var tokens = []
        // in case of strings like foo/bar
        if (slashes[0] !== 0) return
        for (var i = 0; i < slashes.length; i++) {
            if (typeof slashes[i] == 'number')
                tokens.push(argString.substring(slashes[i] + 1, slashes[i + 1]))
        }
        return tokens
    }

    function findUnescapedSeparators(str, separator) {
        if (!separator) separator = '/'

        var escapeNextChar = false
        var slashes = []
        for (var i = 0; i < str.length; i++) {
            var c = str.charAt(i)
            if (!escapeNextChar && c == separator) {
                slashes.push(i)
            }
            escapeNextChar = !escapeNextChar && c == '\\'
        }
        return slashes
    }

    // Translates a search string from ex (vim) syntax into javascript form.
    function translateRegex(str) {
        // When these match, add a '\' if unescaped or remove one if escaped.
        var specials = '|(){'
        // Remove, but never add, a '\' for these.
        var unescape = '}'
        var escapeNextChar = false
        var out = []
        for (var i = -1; i < str.length; i++) {
            var c = str.charAt(i) || ''
            var n = str.charAt(i + 1) || ''
            var specialComesNext = n && specials.indexOf(n) != -1
            if (escapeNextChar) {
                if (c !== '\\' || !specialComesNext) {
                    out.push(c)
                }
                escapeNextChar = false
            } else {
                if (c === '\\') {
                    escapeNextChar = true
                    // Treat the unescape list as special for removing, but not adding '\'.
                    if (n && unescape.indexOf(n) != -1) {
                        specialComesNext = true
                    }
                    // Not passing this test means removing a '\'.
                    if (!specialComesNext || n === '\\') {
                        out.push(c)
                    }
                } else {
                    out.push(c)
                    if (specialComesNext && n !== '\\') {
                        out.push('\\')
                    }
                }
            }
        }
        return out.join('')
    }

    // Translates the replace part of a search and replace from ex (vim) syntax into
    // javascript form.  Similar to translateRegex, but additionally fixes back references
    // (translates '\[0..9]' to '$[0..9]') and follows different rules for escaping '$'.
    var charUnescapes = { '\\n': '\n', '\\r': '\r', '\\t': '\t' }
    function translateRegexReplace(str) {
        var escapeNextChar = false
        var out = []
        for (var i = -1; i < str.length; i++) {
            var c = str.charAt(i) || ''
            var n = str.charAt(i + 1) || ''
            if (charUnescapes[c + n]) {
                out.push(charUnescapes[c + n])
                i++
            } else if (escapeNextChar) {
                // At any point in the loop, escapeNextChar is true if the previous
                // character was a '\' and was not escaped.
                out.push(c)
                escapeNextChar = false
            } else {
                if (c === '\\') {
                    escapeNextChar = true
                    if (isNumber(n) || n === '$') {
                        out.push('$')
                    } else if (n !== '/' && n !== '\\') {
                        out.push('\\')
                    }
                } else {
                    if (c === '$') {
                        out.push('$')
                    }
                    out.push(c)
                    if (n === '/') {
                        out.push('\\')
                    }
                }
            }
        }
        return out.join('')
    }

    // Unescape \ and / in the replace part, for PCRE mode.
    var unescapes = {
        '\\/': '/',
        '\\\\': '\\',
        '\\n': '\n',
        '\\r': '\r',
        '\\t': '\t',
        '\\&': '&',
    }
    function unescapeRegexReplace(str) {
        var stream = new CodeMirror.StringStream(str)
        var output = []
        while (!stream.eol()) {
            // Search for \.
            while (stream.peek() && stream.peek() != '\\') {
                output.push(stream.next())
            }
            var matched = false
            for (var matcher in unescapes) {
                if (stream.match(matcher, true)) {
                    matched = true
                    output.push(unescapes[matcher])
                    break
                }
            }
            if (!matched) {
                // Don't change anything
                output.push(stream.next())
            }
        }
        return output.join('')
    }

    /**
     * Extract the regular expression from the query and return a Regexp object.
     * Returns null if the query is blank.
     * If ignoreCase is passed in, the Regexp object will have the 'i' flag set.
     * If smartCase is passed in, and the query contains upper case letters,
     *   then ignoreCase is overridden, and the 'i' flag will not be set.
     * If the query contains the /i in the flag part of the regular expression,
     *   then both ignoreCase and smartCase are ignored, and 'i' will be passed
     *   through to the Regex object.
     */
    function parseQuery(query, ignoreCase, smartCase) {
        // First update the last search register
        var lastSearchRegister =
            vimGlobalState.registerController.getRegister('/')
        lastSearchRegister.setText(query)
        // Check if the query is already a regex.
        if (query instanceof RegExp) {
            return query
        }
        // First try to extract regex + flags from the input. If no flags found,
        // extract just the regex. IE does not accept flags directly defined in
        // the regex string in the form /regex/flags
        var slashes = findUnescapedSlashes(query)
        var regexPart
        var forceIgnoreCase
        if (!slashes.length) {
            // Query looks like 'regexp'
            regexPart = query
        } else {
            // Query looks like 'regexp/...'
            regexPart = query.substring(0, slashes[0])
            var flagsPart = query.substring(slashes[0])
            forceIgnoreCase = flagsPart.indexOf('i') != -1
        }
        if (!regexPart) {
            return null
        }
        if (!getOption('pcre')) {
            regexPart = translateRegex(regexPart)
        }
        if (smartCase) {
            ignoreCase = /^[^A-Z]*$/.test(regexPart)
        }
        var regexp = new RegExp(
            regexPart,
            ignoreCase || forceIgnoreCase ? 'im' : 'm'
        )
        return regexp
    }

    /**
     * dom - Document Object Manipulator
     * Usage:
     *   dom('<tag>'|<node>[, ...{<attributes>|<$styles>}|<child-node>|'<text>'])
     * Examples:
     *   dom('div', {id:'xyz'}, dom('p', 'CM rocks!', {$color:'red'}))
     *   dom(document.head, dom('script', 'alert("hello!")'))
     * Not supported:
     *   dom('p', ['arrays are objects'], Error('objects specify attributes'))
     */
    function dom(n) {
        if (typeof n === 'string') n = document.createElement(n)
        for (var a, i = 1; i < arguments.length; i++) {
            if (!(a = arguments[i])) continue
            if (typeof a !== 'object') a = document.createTextNode(a)
            if (a.nodeType) n.appendChild(a)
            else
                for (var key in a) {
                    if (!Object.prototype.hasOwnProperty.call(a, key)) continue
                    if (key[0] === '$') n.style[key.slice(1)] = a[key]
                    else n.setAttribute(key, a[key])
                }
        }
        return n
    }

    function showConfirm(cm, template) {
        var pre = dom(
            'div',
            { $color: 'red', $whiteSpace: 'pre', class: 'cm-vim-message' },
            template
        )
        if (cm.openNotification) {
            cm.openNotification(pre, { bottom: true, duration: 5000 })
        } else {
            alert(pre.innerText)
        }
    }

    function makePrompt(prefix, desc) {
        return dom(
            document.createDocumentFragment(),
            dom(
                'span',
                { $fontFamily: 'monospace', $whiteSpace: 'pre' },
                prefix,
                dom('input', {
                    type: 'text',
                    autocorrect: 'off',
                    autocapitalize: 'off',
                    spellcheck: 'false',
                })
            ),
            desc && dom('span', { $color: '#888' }, desc)
        )
    }

    function showPrompt(cm, options) {
        var template = makePrompt(options.prefix, options.desc)
        if (cm.openDialog) {
            cm.openDialog(template, options.onClose, {
                onKeyDown: options.onKeyDown,
                onKeyUp: options.onKeyUp,
                bottom: true,
                selectValueOnOpen: false,
                value: options.value,
            })
        } else {
            var shortText = ''
            if (typeof options.prefix != 'string' && options.prefix)
                shortText += options.prefix.textContent
            if (options.desc) shortText += ' ' + options.desc
            options.onClose(prompt(shortText, ''))
        }
    }

    function regexEqual(r1, r2) {
        if (r1 instanceof RegExp && r2 instanceof RegExp) {
            var props = ['global', 'multiline', 'ignoreCase', 'source']
            for (var i = 0; i < props.length; i++) {
                var prop = props[i]
                if (r1[prop] !== r2[prop]) {
                    return false
                }
            }
            return true
        }
        return false
    }
    // Returns true if the query is valid.
    function updateSearchQuery(cm, rawQuery, ignoreCase, smartCase) {
        if (!rawQuery) {
            return
        }
        var state = getSearchState(cm)
        var query = parseQuery(rawQuery, !!ignoreCase, !!smartCase)
        if (!query) {
            return
        }
        highlightSearchMatches(cm, query)
        if (regexEqual(query, state.getQuery())) {
            return query
        }
        state.setQuery(query)
        return query
    }
    function searchOverlay(query) {
        if (query.source.charAt(0) == '^') {
            var matchSol = true
        }
        return {
            token: function (stream) {
                if (matchSol && !stream.sol()) {
                    stream.skipToEnd()
                    return
                }
                var match = stream.match(query, false)
                if (match) {
                    if (match[0].length == 0) {
                        // Matched empty string, skip to next.
                        stream.next()
                        return 'searching'
                    }
                    if (!stream.sol()) {
                        // Backtrack 1 to match \b
                        stream.backUp(1)
                        if (!query.exec(stream.next() + match[0])) {
                            stream.next()
                            return null
                        }
                    }
                    stream.match(query)
                    return 'searching'
                }
                while (!stream.eol()) {
                    stream.next()
                    if (stream.match(query, false)) break
                }
            },
            query: query,
        }
    }
    var highlightTimeout = 0
    function highlightSearchMatches(cm, query) {
        clearTimeout(highlightTimeout)
        highlightTimeout = setTimeout(function () {
            if (!cm.state.vim) return
            var searchState = getSearchState(cm)
            var overlay = searchState.getOverlay()
            if (!overlay || query != overlay.query) {
                if (overlay) {
                    cm.removeOverlay(overlay)
                }
                overlay = searchOverlay(query)
                cm.addOverlay(overlay)
                if (cm.showMatchesOnScrollbar) {
                    if (searchState.getScrollbarAnnotate()) {
                        searchState.getScrollbarAnnotate().clear()
                    }
                    searchState.setScrollbarAnnotate(
                        cm.showMatchesOnScrollbar(query)
                    )
                }
                searchState.setOverlay(overlay)
            }
        }, 50)
    }
    function findNext(cm, prev, query, repeat) {
        if (repeat === undefined) {
            repeat = 1
        }
        return cm.operation(function () {
            var pos = cm.getCursor()
            var cursor = cm.getSearchCursor(query, pos)
            for (var i = 0; i < repeat; i++) {
                var found = cursor.find(prev)
                if (i == 0 && found && cursorEqual(cursor.from(), pos)) {
                    var lastEndPos = prev ? cursor.from() : cursor.to()
                    found = cursor.find(prev)
                    if (
                        found &&
                        !found[0] &&
                        cursorEqual(cursor.from(), lastEndPos)
                    ) {
                        if (cm.getLine(lastEndPos.line).length == lastEndPos.ch)
                            found = cursor.find(prev)
                    }
                }
                if (!found) {
                    // SearchCursor may have returned null because it hit EOF, wrap
                    // around and try again.
                    cursor = cm.getSearchCursor(
                        query,
                        prev
                            ? new Pos(cm.lastLine())
                            : new Pos(cm.firstLine(), 0)
                    )
                    if (!cursor.find(prev)) {
                        return
                    }
                }
            }
            return cursor.from()
        })
    }
    /**
     * Pretty much the same as `findNext`, except for the following differences:
     *
     * 1. Before starting the search, move to the previous search. This way if our cursor is
     * already inside a match, we should return the current match.
     * 2. Rather than only returning the cursor's from, we return the cursor's from and to as a tuple.
     */
    function findNextFromAndToInclusive(cm, prev, query, repeat, vim) {
        if (repeat === undefined) {
            repeat = 1
        }
        return cm.operation(function () {
            var pos = cm.getCursor()
            var cursor = cm.getSearchCursor(query, pos)

            // Go back one result to ensure that if the cursor is currently a match, we keep it.
            var found = cursor.find(!prev)

            // If we haven't moved, go back one more (similar to if i==0 logic in findNext).
            if (!vim.visualMode && found && cursorEqual(cursor.from(), pos)) {
                cursor.find(!prev)
            }

            for (var i = 0; i < repeat; i++) {
                found = cursor.find(prev)
                if (!found) {
                    // SearchCursor may have returned null because it hit EOF, wrap
                    // around and try again.
                    cursor = cm.getSearchCursor(
                        query,
                        prev
                            ? new Pos(cm.lastLine())
                            : new Pos(cm.firstLine(), 0)
                    )
                    if (!cursor.find(prev)) {
                        return
                    }
                }
            }
            return [cursor.from(), cursor.to()]
        })
    }
    function clearSearchHighlight(cm) {
        var state = getSearchState(cm)
        cm.removeOverlay(getSearchState(cm).getOverlay())
        state.setOverlay(null)
        if (state.getScrollbarAnnotate()) {
            state.getScrollbarAnnotate().clear()
            state.setScrollbarAnnotate(null)
        }
    }
    /**
     * Check if pos is in the specified range, INCLUSIVE.
     * Range can be specified with 1 or 2 arguments.
     * If the first range argument is an array, treat it as an array of line
     * numbers. Match pos against any of the lines.
     * If the first range argument is a number,
     *   if there is only 1 range argument, check if pos has the same line
     *       number
     *   if there are 2 range arguments, then check if pos is in between the two
     *       range arguments.
     */
    function isInRange(pos, start, end) {
        if (typeof pos != 'number') {
            // Assume it is a cursor position. Get the line number.
            pos = pos.line
        }
        if (start instanceof Array) {
            return inArray(pos, start)
        } else {
            if (typeof end == 'number') {
                return pos >= start && pos <= end
            } else {
                return pos == start
            }
        }
    }
    function getUserVisibleLines(cm) {
        var scrollInfo = cm.getScrollInfo()
        var occludeToleranceTop = 6
        var occludeToleranceBottom = 10
        var from = cm.coordsChar(
            { left: 0, top: occludeToleranceTop + scrollInfo.top },
            'local'
        )
        var bottomY =
            scrollInfo.clientHeight - occludeToleranceBottom + scrollInfo.top
        var to = cm.coordsChar({ left: 0, top: bottomY }, 'local')
        return { top: from.line, bottom: to.line }
    }

    function getMarkPos(cm, vim, markName) {
        if (markName == "'" || markName == '`') {
            return vimGlobalState.jumpList.find(cm, -1) || new Pos(0, 0)
        } else if (markName == '.') {
            return getLastEditPos(cm)
        }

        var mark = vim.marks[markName]
        return mark && mark.find()
    }

    function getLastEditPos(cm) {
        var done = cm.doc.history.done
        for (var i = done.length; i--; ) {
            if (done[i].changes) {
                return copyCursor(done[i].changes[0].to)
            }
        }
    }

    var ExCommandDispatcher = function () {
        this.buildCommandMap_()
    }
    ExCommandDispatcher.prototype = {
        processCommand: function (cm, input, opt_params) {
            var that = this
            cm.operation(function () {
                cm.curOp.isVimOp = true
                that._processCommand(cm, input, opt_params)
            })
        },
        _processCommand: function (cm, input, opt_params) {
            var vim = cm.state.vim
            var commandHistoryRegister =
                vimGlobalState.registerController.getRegister(':')
            var previousCommand = commandHistoryRegister.toString()
            if (vim.visualMode) {
                exitVisualMode(cm)
            }
            var inputStream = new CodeMirror.StringStream(input)
            // update ": with the latest command whether valid or invalid
            commandHistoryRegister.setText(input)
            var params = opt_params || {}
            params.input = input
            try {
                this.parseInput_(cm, inputStream, params)
            } catch (e) {
                showConfirm(cm, e.toString())
                throw e
            }
            var command
            var commandName
            if (!params.commandName) {
                // If only a line range is defined, move to the line.
                if (params.line !== undefined) {
                    commandName = 'move'
                }
            } else {
                command = this.matchCommand_(params.commandName)
                if (command) {
                    commandName = command.name
                    if (command.excludeFromCommandHistory) {
                        commandHistoryRegister.setText(previousCommand)
                    }
                    this.parseCommandArgs_(inputStream, params, command)
                    if (command.type == 'exToKey') {
                        // Handle Ex to Key mapping.
                        for (var i = 0; i < command.toKeys.length; i++) {
                            vimApi.handleKey(cm, command.toKeys[i], 'mapping')
                        }
                        return
                    } else if (command.type == 'exToEx') {
                        // Handle Ex to Ex mapping.
                        this.processCommand(cm, command.toInput)
                        return
                    }
                }
            }
            if (!commandName) {
                showConfirm(cm, 'Not an editor command ":' + input + '"')
                return
            }
            try {
                exCommands[commandName](cm, params)
                // Possibly asynchronous commands (e.g. substitute, which might have a
                // user confirmation), are responsible for calling the callback when
                // done. All others have it taken care of for them here.
                if ((!command || !command.possiblyAsync) && params.callback) {
                    params.callback()
                }
            } catch (e) {
                showConfirm(cm, e.toString())
                throw e
            }
        },
        parseInput_: function (cm, inputStream, result) {
            inputStream.eatWhile(':')
            // Parse range.
            if (inputStream.eat('%')) {
                result.line = cm.firstLine()
                result.lineEnd = cm.lastLine()
            } else {
                result.line = this.parseLineSpec_(cm, inputStream)
                if (result.line !== undefined && inputStream.eat(',')) {
                    result.lineEnd = this.parseLineSpec_(cm, inputStream)
                }
            }

            // Parse command name.
            var commandMatch = inputStream.match(/^(\w+|!!|@@|[!#&*<=>@~])/)
            if (commandMatch) {
                result.commandName = commandMatch[1]
            } else {
                result.commandName = inputStream.match(/.*/)[0]
            }

            return result
        },
        parseLineSpec_: function (cm, inputStream) {
            var numberMatch = inputStream.match(/^(\d+)/)
            if (numberMatch) {
                // Absolute line number plus offset (N+M or N-M) is probably a typo,
                // not something the user actually wanted. (NB: vim does allow this.)
                return parseInt(numberMatch[1], 10) - 1
            }
            switch (inputStream.next()) {
                case '.':
                    return this.parseLineSpecOffset_(
                        inputStream,
                        cm.getCursor().line
                    )
                case '$':
                    return this.parseLineSpecOffset_(inputStream, cm.lastLine())
                case "'":
                    var markName = inputStream.next()
                    var markPos = getMarkPos(cm, cm.state.vim, markName)
                    if (!markPos) throw new Error('Mark not set')
                    return this.parseLineSpecOffset_(inputStream, markPos.line)
                case '-':
                case '+':
                    inputStream.backUp(1)
                    // Offset is relative to current line if not otherwise specified.
                    return this.parseLineSpecOffset_(
                        inputStream,
                        cm.getCursor().line
                    )
                default:
                    inputStream.backUp(1)
                    return undefined
            }
        },
        parseLineSpecOffset_: function (inputStream, line) {
            var offsetMatch = inputStream.match(/^([+-])?(\d+)/)
            if (offsetMatch) {
                var offset = parseInt(offsetMatch[2], 10)
                if (offsetMatch[1] == '-') {
                    line -= offset
                } else {
                    line += offset
                }
            }
            return line
        },
        parseCommandArgs_: function (inputStream, params, command) {
            if (inputStream.eol()) {
                return
            }
            params.argString = inputStream.match(/.*/)[0]
            // Parse command-line arguments
            var delim = command.argDelimiter || /\s+/
            var args = trim(params.argString).split(delim)
            if (args.length && args[0]) {
                params.args = args
            }
        },
        matchCommand_: function (commandName) {
            // Return the command in the command map that matches the shortest
            // prefix of the passed in command name. The match is guaranteed to be
            // unambiguous if the defaultExCommandMap's shortNames are set up
            // correctly. (see @code{defaultExCommandMap}).
            for (var i = commandName.length; i > 0; i--) {
                var prefix = commandName.substring(0, i)
                if (this.commandMap_[prefix]) {
                    var command = this.commandMap_[prefix]
                    if (command.name.indexOf(commandName) === 0) {
                        return command
                    }
                }
            }
            return null
        },
        buildCommandMap_: function () {
            this.commandMap_ = {}
            for (var i = 0; i < defaultExCommandMap.length; i++) {
                var command = defaultExCommandMap[i]
                var key = command.shortName || command.name
                this.commandMap_[key] = command
            }
        },
        map: function (lhs, rhs, ctx) {
            if (lhs != ':' && lhs.charAt(0) == ':') {
                if (ctx) {
                    throw Error('Mode not supported for ex mappings')
                }
                var commandName = lhs.substring(1)
                if (rhs != ':' && rhs.charAt(0) == ':') {
                    // Ex to Ex mapping
                    this.commandMap_[commandName] = {
                        name: commandName,
                        type: 'exToEx',
                        toInput: rhs.substring(1),
                        user: true,
                    }
                } else {
                    // Ex to key mapping
                    this.commandMap_[commandName] = {
                        name: commandName,
                        type: 'exToKey',
                        toKeys: rhs,
                        user: true,
                    }
                }
            } else {
                if (rhs != ':' && rhs.charAt(0) == ':') {
                    // Key to Ex mapping.
                    var mapping = {
                        keys: lhs,
                        type: 'keyToEx',
                        exArgs: { input: rhs.substring(1) },
                    }
                    if (ctx) {
                        mapping.context = ctx
                    }
                    defaultKeymap.unshift(mapping)
                } else {
                    // Key to key mapping
                    var mapping = {
                        keys: lhs,
                        type: 'keyToKey',
                        toKeys: rhs,
                    }
                    if (ctx) {
                        mapping.context = ctx
                    }
                    defaultKeymap.unshift(mapping)
                }
            }
        },
        unmap: function (lhs, ctx) {
            if (lhs != ':' && lhs.charAt(0) == ':') {
                // Ex to Ex or Ex to key mapping
                if (ctx) {
                    throw Error('Mode not supported for ex mappings')
                }
                var commandName = lhs.substring(1)
                if (
                    this.commandMap_[commandName] &&
                    this.commandMap_[commandName].user
                ) {
                    delete this.commandMap_[commandName]
                    return true
                }
            } else {
                // Key to Ex or key to key mapping
                var keys = lhs
                for (var i = 0; i < defaultKeymap.length; i++) {
                    if (
                        keys == defaultKeymap[i].keys &&
                        defaultKeymap[i].context === ctx
                    ) {
                        defaultKeymap.splice(i, 1)
                        return true
                    }
                }
            }
        },
    }

    var exCommands = {
        colorscheme: function (cm, params) {
            if (!params.args || params.args.length < 1) {
                showConfirm(cm, cm.getOption('theme'))
                return
            }
            cm.setOption('theme', params.args[0])
        },
        map: function (cm, params, ctx) {
            var mapArgs = params.args
            if (!mapArgs || mapArgs.length < 2) {
                if (cm) {
                    showConfirm(cm, 'Invalid mapping: ' + params.input)
                }
                return
            }
            exCommandDispatcher.map(mapArgs[0], mapArgs[1], ctx)
        },
        imap: function (cm, params) {
            this.map(cm, params, 'insert')
        },
        nmap: function (cm, params) {
            this.map(cm, params, 'normal')
        },
        vmap: function (cm, params) {
            this.map(cm, params, 'visual')
        },
        unmap: function (cm, params, ctx) {
            var mapArgs = params.args
            if (
                !mapArgs ||
                mapArgs.length < 1 ||
                !exCommandDispatcher.unmap(mapArgs[0], ctx)
            ) {
                if (cm) {
                    showConfirm(cm, 'No such mapping: ' + params.input)
                }
            }
        },
        move: function (cm, params) {
            commandDispatcher.processCommand(cm, cm.state.vim, {
                type: 'motion',
                motion: 'moveToLineOrEdgeOfDocument',
                motionArgs: {
                    forward: false,
                    explicitRepeat: true,
                    linewise: true,
                },
                repeatOverride: params.line + 1,
            })
        },
        set: function (cm, params) {
            var setArgs = params.args
            // Options passed through to the setOption/getOption calls. May be passed in by the
            // local/global versions of the set command
            var setCfg = params.setCfg || {}
            if (!setArgs || setArgs.length < 1) {
                if (cm) {
                    showConfirm(cm, 'Invalid mapping: ' + params.input)
                }
                return
            }
            var expr = setArgs[0].split('=')
            var optionName = expr[0]
            var value = expr[1]
            var forceGet = false

            if (optionName.charAt(optionName.length - 1) == '?') {
                // If post-fixed with ?, then the set is actually a get.
                if (value) {
                    throw Error('Trailing characters: ' + params.argString)
                }
                optionName = optionName.substring(0, optionName.length - 1)
                forceGet = true
            }
            if (value === undefined && optionName.substring(0, 2) == 'no') {
                // To set boolean options to false, the option name is prefixed with
                // 'no'.
                optionName = optionName.substring(2)
                value = false
            }

            var optionIsBoolean =
                options[optionName] && options[optionName].type == 'boolean'
            if (optionIsBoolean && value == undefined) {
                // Calling set with a boolean option sets it to true.
                value = true
            }
            // If no value is provided, then we assume this is a get.
            if ((!optionIsBoolean && value === undefined) || forceGet) {
                var oldValue = getOption(optionName, cm, setCfg)
                if (oldValue instanceof Error) {
                    showConfirm(cm, oldValue.message)
                } else if (oldValue === true || oldValue === false) {
                    showConfirm(cm, ' ' + (oldValue ? '' : 'no') + optionName)
                } else {
                    showConfirm(cm, '  ' + optionName + '=' + oldValue)
                }
            } else {
                var setOptionReturn = setOption(optionName, value, cm, setCfg)
                if (setOptionReturn instanceof Error) {
                    showConfirm(cm, setOptionReturn.message)
                }
            }
        },
        setlocal: function (cm, params) {
            // setCfg is passed through to setOption
            params.setCfg = { scope: 'local' }
            this.set(cm, params)
        },
        setglobal: function (cm, params) {
            // setCfg is passed through to setOption
            params.setCfg = { scope: 'global' }
            this.set(cm, params)
        },
        registers: function (cm, params) {
            var regArgs = params.args
            var registers = vimGlobalState.registerController.registers
            var regInfo = '----------Registers----------\n\n'
            if (!regArgs) {
                for (var registerName in registers) {
                    var text = registers[registerName].toString()
                    if (text.length) {
                        regInfo += '"' + registerName + '    ' + text + '\n'
                    }
                }
            } else {
                var registerName
                regArgs = regArgs.join('')
                for (var i = 0; i < regArgs.length; i++) {
                    registerName = regArgs.charAt(i)
                    if (
                        !vimGlobalState.registerController.isValidRegister(
                            registerName
                        )
                    ) {
                        continue
                    }
                    var register = registers[registerName] || new Register()
                    regInfo +=
                        '"' + registerName + '    ' + register.toString() + '\n'
                }
            }
            showConfirm(cm, regInfo)
        },
        sort: function (cm, params) {
            var reverse, ignoreCase, unique, number, pattern
            function parseArgs() {
                if (params.argString) {
                    var args = new CodeMirror.StringStream(params.argString)
                    if (args.eat('!')) {
                        reverse = true
                    }
                    if (args.eol()) {
                        return
                    }
                    if (!args.eatSpace()) {
                        return 'Invalid arguments'
                    }
                    var opts = args.match(/([dinuox]+)?\s*(\/.+\/)?\s*/)
                    if (!opts && !args.eol()) {
                        return 'Invalid arguments'
                    }
                    if (opts[1]) {
                        ignoreCase = opts[1].indexOf('i') != -1
                        unique = opts[1].indexOf('u') != -1
                        var decimal =
                            opts[1].indexOf('d') != -1 ||
                            (opts[1].indexOf('n') != -1 && 1)
                        var hex = opts[1].indexOf('x') != -1 && 1
                        var octal = opts[1].indexOf('o') != -1 && 1
                        if (decimal + hex + octal > 1) {
                            return 'Invalid arguments'
                        }
                        number =
                            (decimal && 'decimal') ||
                            (hex && 'hex') ||
                            (octal && 'octal')
                    }
                    if (opts[2]) {
                        pattern = new RegExp(
                            opts[2].substr(1, opts[2].length - 2),
                            ignoreCase ? 'i' : ''
                        )
                    }
                }
            }
            var err = parseArgs()
            if (err) {
                showConfirm(cm, err + ': ' + params.argString)
                return
            }
            var lineStart = params.line || cm.firstLine()
            var lineEnd = params.lineEnd || params.line || cm.lastLine()
            if (lineStart == lineEnd) {
                return
            }
            var curStart = new Pos(lineStart, 0)
            var curEnd = new Pos(lineEnd, lineLength(cm, lineEnd))
            var text = cm.getRange(curStart, curEnd).split('\n')
            var numberRegex = pattern
                ? pattern
                : number == 'decimal'
                ? /(-?)([\d]+)/
                : number == 'hex'
                ? /(-?)(?:0x)?([0-9a-f]+)/i
                : number == 'octal'
                ? /([0-7]+)/
                : null
            var radix =
                number == 'decimal'
                    ? 10
                    : number == 'hex'
                    ? 16
                    : number == 'octal'
                    ? 8
                    : null
            var numPart = [],
                textPart = []
            if (number || pattern) {
                for (var i = 0; i < text.length; i++) {
                    var matchPart = pattern ? text[i].match(pattern) : null
                    if (matchPart && matchPart[0] != '') {
                        numPart.push(matchPart)
                    } else if (!pattern && numberRegex.exec(text[i])) {
                        numPart.push(text[i])
                    } else {
                        textPart.push(text[i])
                    }
                }
            } else {
                textPart = text
            }
            function compareFn(a, b) {
                if (reverse) {
                    var tmp
                    tmp = a
                    a = b
                    b = tmp
                }
                if (ignoreCase) {
                    a = a.toLowerCase()
                    b = b.toLowerCase()
                }
                var anum = number && numberRegex.exec(a)
                var bnum = number && numberRegex.exec(b)
                if (!anum) {
                    return a < b ? -1 : 1
                }
                anum = parseInt((anum[1] + anum[2]).toLowerCase(), radix)
                bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix)
                return anum - bnum
            }
            function comparePatternFn(a, b) {
                if (reverse) {
                    var tmp
                    tmp = a
                    a = b
                    b = tmp
                }
                if (ignoreCase) {
                    a[0] = a[0].toLowerCase()
                    b[0] = b[0].toLowerCase()
                }
                return a[0] < b[0] ? -1 : 1
            }
            numPart.sort(pattern ? comparePatternFn : compareFn)
            if (pattern) {
                for (var i = 0; i < numPart.length; i++) {
                    numPart[i] = numPart[i].input
                }
            } else if (!number) {
                textPart.sort(compareFn)
            }
            text = !reverse
                ? textPart.concat(numPart)
                : numPart.concat(textPart)
            if (unique) {
                // Remove duplicate lines
                var textOld = text
                var lastLine
                text = []
                for (var i = 0; i < textOld.length; i++) {
                    if (textOld[i] != lastLine) {
                        text.push(textOld[i])
                    }
                    lastLine = textOld[i]
                }
            }
            cm.replaceRange(text.join('\n'), curStart, curEnd)
        },
        vglobal: function (cm, params) {
            // global inspects params.commandName
            this.global(cm, params)
        },
        global: function (cm, params) {
            // a global command is of the form
            // :[range]g/pattern/[cmd]
            // argString holds the string /pattern/[cmd]
            var argString = params.argString
            if (!argString) {
                showConfirm(cm, 'Regular Expression missing from global')
                return
            }
            var inverted = params.commandName[0] === 'v'
            // range is specified here
            var lineStart =
                params.line !== undefined ? params.line : cm.firstLine()
            var lineEnd = params.lineEnd || params.line || cm.lastLine()
            // get the tokens from argString
            var tokens = splitBySlash(argString)
            var regexPart = argString,
                cmd
            if (tokens.length) {
                regexPart = tokens[0]
                cmd = tokens.slice(1, tokens.length).join('/')
            }
            if (regexPart) {
                // If regex part is empty, then use the previous query. Otherwise
                // use the regex part as the new query.
                try {
                    updateSearchQuery(
                        cm,
                        regexPart,
                        true /** ignoreCase */,
                        true /** smartCase */
                    )
                } catch (e) {
                    showConfirm(cm, 'Invalid regex: ' + regexPart)
                    return
                }
            }
            // now that we have the regexPart, search for regex matches in the
            // specified range of lines
            var query = getSearchState(cm).getQuery()
            var matchedLines = []
            for (var i = lineStart; i <= lineEnd; i++) {
                var line = cm.getLineHandle(i)
                var matched = query.test(line.text)
                if (matched !== inverted) {
                    matchedLines.push(cmd ? line : line.text)
                }
            }
            // if there is no [cmd], just display the list of matched lines
            if (!cmd) {
                showConfirm(cm, matchedLines.join('\n'))
                return
            }
            var index = 0
            var nextCommand = function () {
                if (index < matchedLines.length) {
                    var line = matchedLines[index++]
                    var lineNum = cm.getLineNumber(line)
                    if (lineNum == null) {
                        nextCommand()
                        return
                    }
                    var command = lineNum + 1 + cmd
                    exCommandDispatcher.processCommand(cm, command, {
                        callback: nextCommand,
                    })
                }
            }
            nextCommand()
        },
        substitute: function (cm, params) {
            if (!cm.getSearchCursor) {
                throw new Error(
                    'Search feature not available. Requires searchcursor.js or ' +
                        'any other getSearchCursor implementation.'
                )
            }
            var argString = params.argString
            var tokens = argString
                ? splitBySeparator(argString, argString[0])
                : []
            var regexPart,
                replacePart = '',
                trailing,
                flagsPart,
                count
            var confirm = false // Whether to confirm each replace.
            var global = false // True to replace all instances on a line, false to replace only 1.
            if (tokens.length) {
                regexPart = tokens[0]
                if (getOption('pcre') && regexPart !== '') {
                    regexPart = new RegExp(regexPart).source //normalize not escaped characters
                }
                replacePart = tokens[1]
                if (replacePart !== undefined) {
                    if (getOption('pcre')) {
                        replacePart = unescapeRegexReplace(
                            replacePart.replace(/([^\\])&/g, '$1$$&')
                        )
                    } else {
                        replacePart = translateRegexReplace(replacePart)
                    }
                    vimGlobalState.lastSubstituteReplacePart = replacePart
                }
                trailing = tokens[2] ? tokens[2].split(' ') : []
            } else {
                // either the argString is empty or its of the form ' hello/world'
                // actually splitBySlash returns a list of tokens
                // only if the string starts with a '/'
                if (argString && argString.length) {
                    showConfirm(
                        cm,
                        'Substitutions should be of the form ' +
                            ':s/pattern/replace/'
                    )
                    return
                }
            }
            // After the 3rd slash, we can have flags followed by a space followed
            // by count.
            if (trailing) {
                flagsPart = trailing[0]
                count = parseInt(trailing[1])
                if (flagsPart) {
                    if (flagsPart.indexOf('c') != -1) {
                        confirm = true
                    }
                    if (flagsPart.indexOf('g') != -1) {
                        global = true
                    }
                    if (getOption('pcre')) {
                        regexPart = regexPart + '/' + flagsPart
                    } else {
                        regexPart =
                            regexPart.replace(/\//g, '\\/') + '/' + flagsPart
                    }
                }
            }
            if (regexPart) {
                // If regex part is empty, then use the previous query. Otherwise use
                // the regex part as the new query.
                try {
                    updateSearchQuery(
                        cm,
                        regexPart,
                        true /** ignoreCase */,
                        true /** smartCase */
                    )
                } catch (e) {
                    showConfirm(cm, 'Invalid regex: ' + regexPart)
                    return
                }
            }
            replacePart =
                replacePart || vimGlobalState.lastSubstituteReplacePart
            if (replacePart === undefined) {
                showConfirm(cm, 'No previous substitute regular expression')
                return
            }
            var state = getSearchState(cm)
            var query = state.getQuery()
            var lineStart =
                params.line !== undefined ? params.line : cm.getCursor().line
            var lineEnd = params.lineEnd || lineStart
            if (lineStart == cm.firstLine() && lineEnd == cm.lastLine()) {
                lineEnd = Infinity
            }
            if (count) {
                lineStart = lineEnd
                lineEnd = lineStart + count - 1
            }
            var startPos = clipCursorToContent(cm, new Pos(lineStart, 0))
            var cursor = cm.getSearchCursor(query, startPos)
            doReplace(
                cm,
                confirm,
                global,
                lineStart,
                lineEnd,
                cursor,
                query,
                replacePart,
                params.callback
            )
        },
        redo: CodeMirror.commands.redo,
        undo: CodeMirror.commands.undo,
        write: function (cm) {
            if (CodeMirror.commands.save) {
                // If a save command is defined, call it.
                CodeMirror.commands.save(cm)
            } else if (cm.save) {
                // Saves to text area if no save command is defined and cm.save() is available.
                cm.save()
            }
        },
        nohlsearch: function (cm) {
            clearSearchHighlight(cm)
        },
        yank: function (cm) {
            var cur = copyCursor(cm.getCursor())
            var line = cur.line
            var lineText = cm.getLine(line)
            vimGlobalState.registerController.pushText(
                '0',
                'yank',
                lineText,
                true,
                true
            )
        },
        delmarks: function (cm, params) {
            if (!params.argString || !trim(params.argString)) {
                showConfirm(cm, 'Argument required')
                return
            }

            var state = cm.state.vim
            var stream = new CodeMirror.StringStream(trim(params.argString))
            while (!stream.eol()) {
                stream.eatSpace()

                // Record the streams position at the beginning of the loop for use
                // in error messages.
                var count = stream.pos

                if (!stream.match(/[a-zA-Z]/, false)) {
                    showConfirm(
                        cm,
                        'Invalid argument: ' + params.argString.substring(count)
                    )
                    return
                }

                var sym = stream.next()
                // Check if this symbol is part of a range
                if (stream.match('-', true)) {
                    // This symbol is part of a range.

                    // The range must terminate at an alphabetic character.
                    if (!stream.match(/[a-zA-Z]/, false)) {
                        showConfirm(
                            cm,
                            'Invalid argument: ' +
                                params.argString.substring(count)
                        )
                        return
                    }

                    var startMark = sym
                    var finishMark = stream.next()
                    // The range must terminate at an alphabetic character which
                    // shares the same case as the start of the range.
                    if (
                        (isLowerCase(startMark) && isLowerCase(finishMark)) ||
                        (isUpperCase(startMark) && isUpperCase(finishMark))
                    ) {
                        var start = startMark.charCodeAt(0)
                        var finish = finishMark.charCodeAt(0)
                        if (start >= finish) {
                            showConfirm(
                                cm,
                                'Invalid argument: ' +
                                    params.argString.substring(count)
                            )
                            return
                        }

                        // Because marks are always ASCII values, and we have
                        // determined that they are the same case, we can use
                        // their char codes to iterate through the defined range.
                        for (var j = 0; j <= finish - start; j++) {
                            var mark = String.fromCharCode(start + j)
                            delete state.marks[mark]
                        }
                    } else {
                        showConfirm(cm, 'Invalid argument: ' + startMark + '-')
                        return
                    }
                } else {
                    // This symbol is a valid mark, and is not part of a range.
                    delete state.marks[sym]
                }
            }
        },
    }

    var exCommandDispatcher = new ExCommandDispatcher()

    /**
     * @param {CodeMirror} cm CodeMirror instance we are in.
     * @param {boolean} confirm Whether to confirm each replace.
     * @param {Cursor} lineStart Line to start replacing from.
     * @param {Cursor} lineEnd Line to stop replacing at.
     * @param {RegExp} query Query for performing matches with.
     * @param {string} replaceWith Text to replace matches with. May contain $1,
     *     $2, etc for replacing captured groups using JavaScript replace.
     * @param {function()} callback A callback for when the replace is done.
     */
    function doReplace(
        cm,
        confirm,
        global,
        lineStart,
        lineEnd,
        searchCursor,
        query,
        replaceWith,
        callback
    ) {
        // Set up all the functions.
        cm.state.vim.exMode = true
        var done = false
        var lastPos, modifiedLineNumber, joined
        function replaceAll() {
            cm.operation(function () {
                while (!done) {
                    replace()
                    next()
                }
                stop()
            })
        }
        function replace() {
            var text = cm.getRange(searchCursor.from(), searchCursor.to())
            var newText = text.replace(query, replaceWith)
            var unmodifiedLineNumber = searchCursor.to().line
            searchCursor.replace(newText)
            modifiedLineNumber = searchCursor.to().line
            lineEnd += modifiedLineNumber - unmodifiedLineNumber
            joined = modifiedLineNumber < unmodifiedLineNumber
        }
        function findNextValidMatch() {
            var lastMatchTo = lastPos && copyCursor(searchCursor.to())
            var match = searchCursor.findNext()
            if (
                match &&
                !match[0] &&
                lastMatchTo &&
                cursorEqual(searchCursor.from(), lastMatchTo)
            ) {
                match = searchCursor.findNext()
            }
            return match
        }
        function next() {
            // The below only loops to skip over multiple occurrences on the same
            // line when 'global' is not true.
            while (
                findNextValidMatch() &&
                isInRange(searchCursor.from(), lineStart, lineEnd)
            ) {
                if (
                    !global &&
                    searchCursor.from().line == modifiedLineNumber &&
                    !joined
                ) {
                    continue
                }
                cm.scrollIntoView(searchCursor.from(), 30)
                cm.setSelection(searchCursor.from(), searchCursor.to())
                lastPos = searchCursor.from()
                done = false
                return
            }
            done = true
        }
        function stop(close) {
            if (close) {
                close()
            }
            cm.focus()
            if (lastPos) {
                cm.setCursor(lastPos)
                var vim = cm.state.vim
                vim.exMode = false
                vim.lastHPos = vim.lastHSPos = lastPos.ch
            }
            if (callback) {
                callback()
            }
        }
        function onPromptKeyDown(e, _value, close) {
            // Swallow all keys.
            CodeMirror.e_stop(e)
            var keyName = CodeMirror.keyName(e)
            switch (keyName) {
                case 'Y':
                    replace()
                    next()
                    break
                case 'N':
                    next()
                    break
                case 'A':
                    // replaceAll contains a call to close of its own. We don't want it
                    // to fire too early or multiple times.
                    var savedCallback = callback
                    callback = undefined
                    cm.operation(replaceAll)
                    callback = savedCallback
                    break
                case 'L':
                    replace()
                // fall through and exit.
                case 'Q':
                case 'Esc':
                case 'Ctrl-C':
                case 'Ctrl-[':
                    stop(close)
                    break
            }
            if (done) {
                stop(close)
            }
            return true
        }

        // Actually do replace.
        next()
        if (done) {
            showConfirm(cm, 'No matches for ' + query.source)
            return
        }
        if (!confirm) {
            replaceAll()
            if (callback) {
                callback()
            }
            return
        }
        showPrompt(cm, {
            prefix: dom(
                'span',
                'replace with ',
                dom('strong', replaceWith),
                ' (y/n/a/q/l)'
            ),
            onKeyDown: onPromptKeyDown,
        })
    }

    CodeMirror.keyMap.vim = {
        attach: attachVimMap,
        detach: detachVimMap,
        call: cmKey,
    }

    function exitInsertMode(cm) {
        var vim = cm.state.vim
        var macroModeState = vimGlobalState.macroModeState
        var insertModeChangeRegister =
            vimGlobalState.registerController.getRegister('.')
        var isPlaying = macroModeState.isPlaying
        var lastChange = macroModeState.lastInsertModeChanges
        if (!isPlaying) {
            cm.off('change', onChange)
            CodeMirror.off(
                cm.getInputField(),
                'keydown',
                onKeyEventTargetKeyDown
            )
        }
        if (!isPlaying && vim.insertModeRepeat > 1) {
            // Perform insert mode repeat for commands like 3,a and 3,o.
            repeatLastEdit(
                cm,
                vim,
                vim.insertModeRepeat - 1,
                true /** repeatForInsert */
            )
            vim.lastEditInputState.repeatOverride = vim.insertModeRepeat
        }
        delete vim.insertModeRepeat
        vim.insertMode = false
        cm.setCursor(cm.getCursor().line, cm.getCursor().ch - 1)
        cm.setOption('keyMap', 'vim')
        cm.setOption('disableInput', true)
        cm.toggleOverwrite(false) // exit replace mode if we were in it.
        // update the ". register before exiting insert mode
        insertModeChangeRegister.setText(lastChange.changes.join(''))
        CodeMirror.signal(cm, 'vim-mode-change', { mode: 'normal' })
        if (macroModeState.isRecording) {
            logInsertModeChange(macroModeState)
        }
    }

    function _mapCommand(command) {
        defaultKeymap.unshift(command)
    }

    function mapCommand(keys, type, name, args, extra) {
        var command = { keys: keys, type: type }
        command[type] = name
        command[type + 'Args'] = args
        for (var key in extra) command[key] = extra[key]
        _mapCommand(command)
    }

    // The timeout in milliseconds for the two-character ESC keymap should be
    // adjusted according to your typing speed to prevent false positives.
    defineOption('insertModeEscKeysTimeout', 200, 'number')

    CodeMirror.keyMap['vim-insert'] = {
        // TODO: override navigation keys so that Esc will cancel automatic
        // indentation from o, O, i_<CR>
        fallthrough: ['default'],
        attach: attachVimMap,
        detach: detachVimMap,
        call: cmKey,
    }

    CodeMirror.keyMap['vim-replace'] = {
        Backspace: 'goCharLeft',
        fallthrough: ['vim-insert'],
        attach: attachVimMap,
        detach: detachVimMap,
    }

    function executeMacroRegister(cm, vim, macroModeState, registerName) {
        var register =
            vimGlobalState.registerController.getRegister(registerName)
        if (registerName == ':') {
            // Read-only register containing last Ex command.
            if (register.keyBuffer[0]) {
                exCommandDispatcher.processCommand(cm, register.keyBuffer[0])
            }
            macroModeState.isPlaying = false
            return
        }
        var keyBuffer = register.keyBuffer
        var imc = 0
        macroModeState.isPlaying = true
        macroModeState.replaySearchQueries = register.searchQueries.slice(0)
        for (var i = 0; i < keyBuffer.length; i++) {
            var text = keyBuffer[i]
            var match, key
            while (text) {
                // Pull off one command key, which is either a single character
                // or a special sequence wrapped in '<' and '>', e.g. '<Space>'.
                match = /<\w+-.+?>|<\w+>|./.exec(text)
                key = match[0]
                text = text.substring(match.index + key.length)
                vimApi.handleKey(cm, key, 'macro')
                if (vim.insertMode) {
                    var changes = register.insertModeChanges[imc++].changes
                    vimGlobalState.macroModeState.lastInsertModeChanges.changes =
                        changes
                    repeatInsertModeChanges(cm, changes, 1)
                    exitInsertMode(cm)
                }
            }
        }
        macroModeState.isPlaying = false
    }

    function logKey(macroModeState, key) {
        if (macroModeState.isPlaying) {
            return
        }
        var registerName = macroModeState.latestRegister
        var register =
            vimGlobalState.registerController.getRegister(registerName)
        if (register) {
            register.pushText(key)
        }
    }

    function logInsertModeChange(macroModeState) {
        if (macroModeState.isPlaying) {
            return
        }
        var registerName = macroModeState.latestRegister
        var register =
            vimGlobalState.registerController.getRegister(registerName)
        if (register && register.pushInsertModeChanges) {
            register.pushInsertModeChanges(macroModeState.lastInsertModeChanges)
        }
    }

    function logSearchQuery(macroModeState, query) {
        if (macroModeState.isPlaying) {
            return
        }
        var registerName = macroModeState.latestRegister
        var register =
            vimGlobalState.registerController.getRegister(registerName)
        if (register && register.pushSearchQuery) {
            register.pushSearchQuery(query)
        }
    }

    /**
     * Listens for changes made in insert mode.
     * Should only be active in insert mode.
     */
    function onChange(cm, changeObj) {
        var macroModeState = vimGlobalState.macroModeState
        var lastChange = macroModeState.lastInsertModeChanges
        if (!macroModeState.isPlaying) {
            while (changeObj) {
                lastChange.expectCursorActivityForChange = true
                if (lastChange.ignoreCount > 1) {
                    lastChange.ignoreCount--
                } else if (
                    changeObj.origin == '+input' ||
                    changeObj.origin == 'paste' ||
                    changeObj.origin === undefined /* only in testing */
                ) {
                    var selectionCount = cm.listSelections().length
                    if (selectionCount > 1)
                        lastChange.ignoreCount = selectionCount
                    var text = changeObj.text.join('\n')
                    if (lastChange.maybeReset) {
                        lastChange.changes = []
                        lastChange.maybeReset = false
                    }
                    if (text) {
                        if (cm.state.overwrite && !/\n/.test(text)) {
                            lastChange.changes.push([text])
                        } else {
                            lastChange.changes.push(text)
                        }
                    }
                }
                // Change objects may be chained with next.
                changeObj = changeObj.next
            }
        }
    }

    /**
     * Listens for any kind of cursor activity on CodeMirror.
     */
    function onCursorActivity(cm) {
        var vim = cm.state.vim
        if (vim.insertMode) {
            // Tracking cursor activity in insert mode (for macro support).
            var macroModeState = vimGlobalState.macroModeState
            if (macroModeState.isPlaying) {
                return
            }
            var lastChange = macroModeState.lastInsertModeChanges
            if (lastChange.expectCursorActivityForChange) {
                lastChange.expectCursorActivityForChange = false
            } else {
                // Cursor moved outside the context of an edit. Reset the change.
                lastChange.maybeReset = true
            }
        } else if (!cm.curOp.isVimOp) {
            handleExternalSelection(cm, vim)
        }
    }
    function handleExternalSelection(cm, vim) {
        var anchor = cm.getCursor('anchor')
        var head = cm.getCursor('head')
        // Enter or exit visual mode to match mouse selection.
        if (vim.visualMode && !cm.somethingSelected()) {
            exitVisualMode(cm, false)
        } else if (
            !vim.visualMode &&
            !vim.insertMode &&
            cm.somethingSelected()
        ) {
            vim.visualMode = true
            vim.visualLine = false
            CodeMirror.signal(cm, 'vim-mode-change', { mode: 'visual' })
        }
        if (vim.visualMode) {
            // Bind CodeMirror selection model to vim selection model.
            // Mouse selections are considered visual characterwise.
            var headOffset = !cursorIsBefore(head, anchor) ? -1 : 0
            var anchorOffset = cursorIsBefore(head, anchor) ? -1 : 0
            head = offsetCursor(head, 0, headOffset)
            anchor = offsetCursor(anchor, 0, anchorOffset)
            vim.sel = {
                anchor: anchor,
                head: head,
            }
            updateMark(cm, vim, '<', cursorMin(head, anchor))
            updateMark(cm, vim, '>', cursorMax(head, anchor))
        } else if (!vim.insertMode) {
            // Reset lastHPos if selection was modified by something outside of vim mode e.g. by mouse.
            vim.lastHPos = cm.getCursor().ch
        }
    }

    /** Wrapper for special keys pressed in insert mode */
    function InsertModeKey(keyName) {
        this.keyName = keyName
    }

    /**
     * Handles raw key down events from the text area.
     * - Should only be active in insert mode.
     * - For recording deletes in insert mode.
     */
    function onKeyEventTargetKeyDown(e) {
        var macroModeState = vimGlobalState.macroModeState
        var lastChange = macroModeState.lastInsertModeChanges
        var keyName = CodeMirror.keyName(e)
        if (!keyName) {
            return
        }
        function onKeyFound() {
            if (lastChange.maybeReset) {
                lastChange.changes = []
                lastChange.maybeReset = false
            }
            lastChange.changes.push(new InsertModeKey(keyName))
            return true
        }
        if (
            keyName.indexOf('Delete') != -1 ||
            keyName.indexOf('Backspace') != -1
        ) {
            CodeMirror.lookupKey(keyName, 'vim-insert', onKeyFound)
        }
    }

    /**
     * Repeats the last edit, which includes exactly 1 command and at most 1
     * insert. Operator and motion commands are read from lastEditInputState,
     * while action commands are read from lastEditActionCommand.
     *
     * If repeatForInsert is true, then the function was called by
     * exitInsertMode to repeat the insert mode changes the user just made. The
     * corresponding enterInsertMode call was made with a count.
     */
    function repeatLastEdit(cm, vim, repeat, repeatForInsert) {
        var macroModeState = vimGlobalState.macroModeState
        macroModeState.isPlaying = true
        var isAction = !!vim.lastEditActionCommand
        var cachedInputState = vim.inputState
        function repeatCommand() {
            if (isAction) {
                commandDispatcher.processAction(
                    cm,
                    vim,
                    vim.lastEditActionCommand
                )
            } else {
                commandDispatcher.evalInput(cm, vim)
            }
        }
        function repeatInsert(repeat) {
            if (macroModeState.lastInsertModeChanges.changes.length > 0) {
                // For some reason, repeat cw in desktop VIM does not repeat
                // insert mode changes. Will conform to that behavior.
                repeat = !vim.lastEditActionCommand ? 1 : repeat
                var changeObject = macroModeState.lastInsertModeChanges
                repeatInsertModeChanges(cm, changeObject.changes, repeat)
            }
        }
        vim.inputState = vim.lastEditInputState
        if (isAction && vim.lastEditActionCommand.interlaceInsertRepeat) {
            // o and O repeat have to be interlaced with insert repeats so that the
            // insertions appear on separate lines instead of the last line.
            for (var i = 0; i < repeat; i++) {
                repeatCommand()
                repeatInsert(1)
            }
        } else {
            if (!repeatForInsert) {
                // Hack to get the cursor to end up at the right place. If I is
                // repeated in insert mode repeat, cursor will be 1 insert
                // change set left of where it should be.
                repeatCommand()
            }
            repeatInsert(repeat)
        }
        vim.inputState = cachedInputState
        if (vim.insertMode && !repeatForInsert) {
            // Don't exit insert mode twice. If repeatForInsert is set, then we
            // were called by an exitInsertMode call lower on the stack.
            exitInsertMode(cm)
        }
        macroModeState.isPlaying = false
    }

    function repeatInsertModeChanges(cm, changes, repeat) {
        function keyHandler(binding) {
            if (typeof binding == 'string') {
                CodeMirror.commands[binding](cm)
            } else {
                binding(cm)
            }
            return true
        }
        var head = cm.getCursor('head')
        var visualBlock =
            vimGlobalState.macroModeState.lastInsertModeChanges.visualBlock
        if (visualBlock) {
            // Set up block selection again for repeating the changes.
            selectForInsert(cm, head, visualBlock + 1)
            repeat = cm.listSelections().length
            cm.setCursor(head)
        }
        for (var i = 0; i < repeat; i++) {
            if (visualBlock) {
                cm.setCursor(offsetCursor(head, i, 0))
            }
            for (var j = 0; j < changes.length; j++) {
                var change = changes[j]
                if (change instanceof InsertModeKey) {
                    CodeMirror.lookupKey(
                        change.keyName,
                        'vim-insert',
                        keyHandler
                    )
                } else if (typeof change == 'string') {
                    cm.replaceSelection(change)
                } else {
                    var start = cm.getCursor()
                    var end = offsetCursor(start, 0, change[0].length)
                    cm.replaceRange(change[0], start, end)
                    cm.setCursor(end)
                }
            }
        }
        if (visualBlock) {
            cm.setCursor(offsetCursor(head, 0, 1))
        }
    }

    // multiselect support
    function cloneVimState(state) {
        var n = new state.constructor()
        Object.keys(state).forEach(function (key) {
            var o = state[key]
            if (Array.isArray(o)) o = o.slice()
            else if (o && typeof o == 'object' && o.constructor != Object)
                o = cloneVimState(o)
            n[key] = o
        })
        if (state.sel) {
            n.sel = {
                head: state.sel.head && copyCursor(state.sel.head),
                anchor: state.sel.anchor && copyCursor(state.sel.anchor),
            }
        }
        return n
    }
    function multiSelectHandleKey(cm, key, origin) {
        var isHandled = false
        var vim = vimApi.maybeInitVimState_(cm)
        var visualBlock = vim.visualBlock || vim.wasInVisualBlock

        var wasMultiselect = cm.isInMultiSelectMode()
        if (vim.wasInVisualBlock && !wasMultiselect) {
            vim.wasInVisualBlock = false
        } else if (wasMultiselect && vim.visualBlock) {
            vim.wasInVisualBlock = true
        }

        if (
            key == '<Esc>' &&
            !vim.insertMode &&
            !vim.visualMode &&
            wasMultiselect &&
            vim.status == '<Esc>'
        ) {
            // allow editor to exit multiselect
            clearInputState(cm)
        } else if (
            visualBlock ||
            !wasMultiselect ||
            cm.inVirtualSelectionMode
        ) {
            isHandled = vimApi.handleKey(cm, key, origin)
        } else {
            var old = cloneVimState(vim)
            var changeQueueList = vim.inputState.changeQueueList || []

            cm.operation(function () {
                cm.curOp.isVimOp = true
                var index = 0
                cm.forEachSelection(function () {
                    cm.state.vim.inputState.changeQueue = changeQueueList[index]
                    var head = cm.getCursor('head')
                    var anchor = cm.getCursor('anchor')
                    var headOffset = !cursorIsBefore(head, anchor) ? -1 : 0
                    var anchorOffset = cursorIsBefore(head, anchor) ? -1 : 0
                    head = offsetCursor(head, 0, headOffset)
                    anchor = offsetCursor(anchor, 0, anchorOffset)
                    cm.state.vim.sel.head = head
                    cm.state.vim.sel.anchor = anchor

                    isHandled = vimApi.handleKey(cm, key, origin)
                    if (cm.virtualSelection) {
                        changeQueueList[index] =
                            cm.state.vim.inputState.changeQueue
                        cm.state.vim = cloneVimState(old)
                    }
                    index++
                })
                if (cm.curOp.cursorActivity && !isHandled)
                    cm.curOp.cursorActivity = false
                cm.state.vim = vim
                vim.inputState.changeQueueList = changeQueueList
                vim.inputState.changeQueue = null
            }, true)
        }
        // some commands may bring visualMode and selection out of sync
        if (
            isHandled &&
            !vim.visualMode &&
            !vim.insert &&
            vim.visualMode != cm.somethingSelected()
        ) {
            handleExternalSelection(cm, vim, true)
        }
        return isHandled
    }
    resetVimGlobalState()

    return vimApi
}
